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)