DEV Community

WJH
WJH

Posted on

How to reuse 100% stateless React components?

NOTE: This post is not complete yet.

This post is inspired by the Elm architecture

Before continue reading, please allow me to clarify some assumptions:
(1) Performance does not matter
(2) There are only 2 kinds of props, namely view props and action props.
(3) We also have a bunch of actions and reducer.

The following is an example on how can we reduce duplicated code and boilerplates when dealing with purely stateless component that needs to be reusable.

Basically, this is how I do it:
(1) Declare the view props of the component as an interface
(2) Declare an initialize function for the view props
(3) Declare the action props of the component as an interface (NOTE: each action should return the respective view)
(4) Declare the initialize function for the action props

Example 1: SignUpDialog

Imagine we want to build a sign up dialog that will be reused by a lot of pages.

export interface SignUpDialogView {
  username: string,
  password: string,
  open: boolean
}

export const initSignUpDialogView = (): SignUpDialogView => ({
  username: '',
  password: '',
  open: false
})

export interface SignUpDialogActions {
  onUsernameChange:    (e: React.ChangeEvent<HTMLInputElement>) => SignUpDialogView,
  onPasswordChange:    (e: React.ChangeEvent<HTMLInputElement>) => SignUpDialogView,
  onSignUpButtonClick: () => SignUpDialogView,
  onCancelButtonClick: () => SignUpDialogView
}

export const initSignUpDialogActions = (
  view: SignUpDialogView, 
  update: (view: SignUpDialogView) => void 
) => ({
  onUsernameChange:    e  => update({...view, username: e.target.value}),
  onPasswordChange:    e  => update({...view, password: e.target.value}),
  onSignUpButtonClick: () => update({...view, open: false}),
  onCancelButtonClick: () => update({...view, open: false})
})


export const SignUpDialog: React.FC<{
  view: SignUpDialogView,
  actions: SignUpDialogActions,
}> = (props) => {
  const {view, actions} = props
  return (view.open &&
    <div>
      Username: 
      <input value={view.username} onChange={actions.onUsernameChange}/>
      Password: 
      <input value={view.password} onChange={actions.onPasswordChange}/>
      <button onClick={actions.onSignUpButtonClick}>Sign Up</button>
      <button onClick={actions.onCancelButtonClick}>Cancel</button>
    </div>
  )
}

Let say we want to use the SignUpDialog in BuyPage (which is stateful):

export class BuyPage extends React.Component<{}, {
  signUpDialogView: SignUpDialogView
}> {
  constructor(props) {
    super(props)
    this.state = {
      signUpDialogView: initSignUpDialogView()
    }
  }
  render() {
    const {signUpDialogView} = this.state
    return (
      <div>
       Buy something
       <SignUpDialog 
         views={signUpDialogView}
         actions={initSignUpDialogActions(
           signUpDialogView, 
           signUpDialogView => this.setState({signUpDialogView})
         )}
         />
      </div>
    )
  }

}

By doing so, you will have 100% customisability, which is not possible to achieve using stateful components.

How? We can achieve customisability using the spread operator.

Suppose we want to capitalise the username:

<SignUpDialog 
  views={{
      ...signUpDialogView,
      username: signUpDialogView.username.toUpperCase()
  }}
  actions={initSignUpDialogActions(
    signUpDialogView, 
    signUpDialogView => this.setState({signUpDialogView})
  )}
/>

Example 2: DatePicker

Now, let's look at another more realistic example, suppose we want to create a DatePicker that can be used by others.

This time, I will leave out the implementation details, because I wanted to highlight the concept only.

Similarly, we will follow the 4 steps.

// Declare view props
export interface DatePickerView {
  currentDay: number,
  currentMonth: number,
  currentYear: number
}

// Declare action props
export interface DatePickerActions {
  chooseDate: (date: Date) => DatePickerView
  changeMonth: (month: number) => DatePickerView
}

// Declare init view function
export const initDatePickerView = (): DatePickerView => ({
  // implementation . . .
})

// Declare init action props
export interface initDatePickerActions = (
  view: DatePickerView,
  update: (view: DatePickerView) => void
): DatePickerActions => ({
  // implementation . . .
})

Now, here's the component:

export const DatePickerDialog: React.FC<{
  view: DatePickerView,
  actions: DatePickerActions,
  update: (view: DatePickerView) => void
}> = (props) => {
  // implementation detail
}

Then, when you want to use it in XXXCompnent:

export class XXXComponent extends React.Component<{}, {
  datePickerView: DatePickerDialogView
}> {
   constructor(props) {
     super(props)
     this.state = {
       datePickerView: initDatePickerView()
     }
   }
   public render() {
     const {datePickerView} = this.state
     return (
       <DatePicker
         view={datePickerView}
         actions={initDatePickerActions(
           datePickerView,
           datePickerView => this.setState({datePickerView})
         )}
         />
     )
   }
}

With this approach, the user of DatePicker can even customise the the navigating experience of the calendar, suppose we don't want to allow user to access to the month of July:

export class XXXComponent extends React.Component<{}, {
  datePickerView: DatePickerDialogView
}> {
   constructor(props) {
     super(props)
     this.state = {
       datePickerView: initDatePickerView()
     }
   }
   public render() {
     const {datePickerView} = this.state
     const datePickerActions = initDatePickerActions(
       datePickerView,
       datePickerView => this.setState({datePickerView})
     )
     return (
       <DatePicker
         view={datePickerView}
         actions={{
           ...datePickerActions,
           changeMonth: month => 
             // If's its July, we make it to August
             datePickerActions.changeMonth(month === 7 ? 8 : month)

         }}
         />
     )
   }
}

Top comments (0)