Children Obey Your Parents: React Design Pattern

With most languages and frameworks, the paradigm looks like this: Parent creates child, child does stuff, parent asks child what it did or child raises an event and tells the parent it did something. The parent then does whatever it needs to and lets the child keep doing whatever it wants. It’s easy to bring this paradigm to React, but then you start running into: How does a parent ask the child what its current state is? For example, this Stack Overflow post: Get selectedKey from dropdown office fabric ui. At this point you have two choices, as demonstrated by the two answers on SO (the second one being the gist of what I am going to outline below that I wrote a little while back), you can either work around React or embrace React’s way.

You see, React isn’t really built around parents asking their children for things, or better said, it isn’t build around asking at all. Instead it is built around parents telling their children things and children telling their parents things. A parent monitoring or controlling the child so that the parent always knows about any state it cares about – then it would not need to ask. When you use office fabric components, they tend to give you the choice of “child tell parent and track for itself” and “parent tell child, child tell parent, repeat”. I will follow that pattern here showing how you can let the parent choose, but for this post we will only focus on the latter option:

Parent.tsx:

import { IDropdownOption } from 'office-ui-fabric-react';
import * as React from 'react';

import { ExampleDropdown } from './ExampleDropdown';

interface State {
    exampleDropdown?: IDropdownOption;
}

export class Parent extends React.Component<any, State> {
    state = { } as State;

    onDropdownChange = (keyValue?: IDropdownOption) => {
        if (keyValue && keyValue.key) {
            this.setState({ exampleDropdown: keyValue })
        }
    }

    render() {
        return <ChildView
            onSelectionChange={this.onDropdownChange}
            selected={this.state.exampleDropdown}
        />
    }
}

ChildView.tsx

import { Dropdown as OfficeFabricDropdown, IDropdownOption } from 'office-ui-fabric-react';
import * as React from 'react';

interface Props {
    /** Provide this value to control the state. If left blank it will manage its own state */
    selected?: IDropdownOption,
    onSelectionChange?: (keyValue?: IDropdownOption) => void,
}

interface State {
    currentState?: IDropdownOption,
    items: IDropdownOption[],
    selectedIndex?: number,
}

export class ChildView extends React.Component<Props, State> {
    state: State = {
        items: [
            { key: '1', text: 'one - 1' },
            { key: '2', text: 'two - 2' },
            { key: '3', text: 'three - 3' },
            { key: '4', text: 'four - 4' },
            { key: '5', text: 'five - 5' },
            { key: '6', text: 'six - 6' },
            { key: '7', text: 'seven - 7' },
            { key: '8', text: 'eight - 8' },
            { key: '9', text: 'nine - 9' },
            { key: '10', text: 'ten - 10' },
        ],
    } as State;

    onDropdownChange = (event: React.FormEvent<HTMLDivElement> | any, option: any = {}, index?: number) => {
        this.setState({
            currentState: index || index === 0 ? this.state.items[index] : undefined,
            selectedIndex: index,
        });

        const currentSelected = (index || index === 0) ? this.state.items[index] : undefined;
        this.props.onSelectionChange && this.props.onSelectionChange(currentSelected);
    };

    render(): JSX.Element {
        const selected = this.props.selected && this.props.selected.key ||
            this.state.currentState && this.state.currentState.key;

        return <OfficeFabricDropdown
            {...this.props}
            ariaLabel="Example Dropdown"
            label="Example Dropdown"
            options={this.state.items}
            placeHolder={"Pick a number"}
            selectedKey={selected}
            onChange={this.onDropdownChange}
        />;
    }
}

export default ChildView;

Having the parent control the state has some nice advantages

  • Save and restore state so that when a user leaves the page and comes back the state is as they left it (putting this on my list of posts to write)
  • The parent always know what the child is doing and never need to ask
  • The parent can change the child state based on its own controls or logic
  • Works great with a component that is a composition of other components, so they can affect each other, without having to have any knowledge of each other, just having the parent that is bringing them together direct the information via props to each child component (deserves its own post)
  • The parent can supervise and reject what the child wants to do, such as if the child was put into an invalid state or invalid characters were entered
  • It does not require those “business logic” type items to be embedded in the child if they are really pertaining to the parent

I’m sure there are more advantages than these. It is a great way to write react controls. I recommend writing your components this way from the start, because it is much easier to write them initially like this than to write them all with their own state and then go back and change them – however even if you haven’t done so from the beginning, it is worthwhile to rewrite them once you see you need these advantages. I rewrote many components in my react-advanced-datatable component (which I’ll publish later) after I realized I had to in order to be able to save and load component state, which is pretty amazing once you get it all wired up.

One last note is that when you do this architecture in a complex component that has its own state but also will take state from the parent (such as one that allows many changes before hitting apply), you will also need to add checks in your componentDidMount and componentDidUpdate methods to monitor changes in the props so you can keep your internal state up to date. Let me know if you are interested in a post about this topic and I’ll put it on my list to write.