React Guide to Props - Part II

In the previous article, you were able to read about the basics of React props. In that article, we wrote about things like sending event handling functions as props, spread operators and sending props with cloning children prop. Overall, it was all the standard flow, where the parent passes the props to child components.

Today, we're spicing up the game and covering a bit more advanced themes - how the component can send information to the parent via props, and how components on the same level (under the same parent) can communicate by using props. Note that there are other, more advanced ways to realize communication between components (using more advanced way to manage app state like redux), but in this article, we're talking about props only.

Note: As for all the previous articles, everything below also applies to React Native, as well!

Communication from children to parent

Props are passed in one direction only: from parent to children. However, this doesn't mean that the communication from children to the parent via props is impossible. It can be implemented using 'the basic flow': by passing the prop functions to children. These function props should act as callbacks.

So, let's say that we have a ChildInput component:

class ChildInput extends Component {
    ...
    handleChange = (event) => {
        // This is functional setState!
        this.setState(() => {
            const newValue = event.target.value;
            this.props.updateParent(newValue);
            return {
                value: newValue,
            }
        });
    }
    ...
    render() {
        return (
            <input
                onChange={this.handleChange}
                placeholder={this.props.placeholder}
                type="text"
                value={this.state.value}
            />
        )
    }
}

In our parent component, we get the information on value updates in input using a callback sent as updateParent prop:

class Parent extends Component {
    ...
    saveChange = (field) => (newValue) => {
        // do something with this field's newValue
        // for this example field is "username"
        // newValue is whatever is typed into input
    }
    ...
    render() {
        return (
            <ChildInput
                placeholder={"Input field"}
                updateParent={this.saveChange("username")}
            />
        )
    }
}

So, every time the user types something in input rendered within the ChildInput component, the state of the component will be updated to contain the new value, and the saveChange function from Parent will be called, sending that new value to parent as well. This kind of communication is most commonly used for conditional rendering.

Of course, this callback approach can be used only when there's some event to trigger it. However, if you need to do something about the child that's not event triggered, most likely this information you need shouldn't belong to the child at all.

Communication between same level components

Direct communication between the two components that are on the same level via props is not possible. There is, however, a way for the two same-level components to communicate via props - if these props can be contained and handled in parent state.

On every change in either of the two components, the parent component's state should be updated with the new information from the updating child component. As the parent's state is updated, it will automatically trigger rerendering of both children components.

Let's say we have a TodoList component which renders items in todo list:

class TodoList extends Component {
    render() {
        return (
            <div>
                {
                    this.props.items.map(this.renderItem)
                }
            </div>
        )
    }
}

Besides this component, We also have an AddTodo component, which should add new todo to our list:

class AddTodo extends Component {
    ...
    addItem = () => {
        this.props.addItem(this.state.todo}
        this.setState(() => {
            return {
                todo: ''
            };
        });
    }
    ...
    render() {
        return (
            <div>
                <input
                    onChange={this.handleChange}
                    type="text"
                    value={this.state.todo}
                />
                <button
                    onClick={this.addItem}
                >
                    Add todo
                </button>
            </div>
        )
    }
}

For this to function properly, TodoPage component will have to know about the todo list, store it and update accordingly:

class TodoPage extends Component {
    constructor() {
        super();
        this.state = {
            todoList: []
        };
    }
    ...
    addItem = (newItem) => {
        this.setState(({ todoList }) => {
            todoList.push(newItem);
            return {
                todoList,
            };
        });
    }
    ...
    render() {
        return (
            <div>
                <AddTodo
                    addItem={this.addItem}
                />
                <TodoList
                    items={this.state.todoList}
                />
            </div>
        );
    }
}

When a new todo item is added to a list, parent will be notified about it and update the state accordingly, thus notifying the TodoList to rerender.

Let's take a look at another example, where we have a ChildInput component (the same one that appeared previously in this article!), a Button and a Form, which is a parent. So we have conditions here: if the value of the input is empty, our button should be disabled, else it'll be enabled.

Here's our Button component:

class Button extends Component {
   render() {
        return (
            <button
                disabled={this.props.disabled}
            >
                { this.props.text }
            </button>
        );
    }
}

Form will know the value of ChildInput and pass it to Button whether it's disabled based on this value:

class Form extends Component {
    constructor() {
        super();
        this.state = {
            fieldValue: ""
        };
    }
    ...
    handleInputChange = (fieldValue) => {
        this.setState(() => {
            return {
                fieldValue,
            };
        });
    }
    ...
    render() {
        return (
            <div>
                <ChildInput
                    placeholder={"Form input"}
                    updateParent={this.handleInputChange}
                />
                <Button
                    disabled={!this.state.fieldValue}
                    text={"Form button"}
                />
            </div>
        );
    }
}

Keep in mind that for more complex applications, you won't be handling same-level component communication this way, but with some more advanced mechanisms. We'll talk about some of these in upcoming articles.

We hope you enjoyed our article! Thank you for your time and good luck with the further learning of one of the best JavaScript frameworks!

Part III of the React props series will be coming soon. It's all about the type checking! Subscribe and stay tuned when it arrives!