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)