Skip to content

Latest commit

 

History

History
331 lines (250 loc) · 10.5 KB

lifting-state.md

File metadata and controls

331 lines (250 loc) · 10.5 KB

Lifting State

Unidirectional Data Flow

Learning Objectives

After this lesson, you will be able to:

  • Define unidirectional flow
  • Diagram data in a component hierarchy

What is Unidirectional Data Flow?

Let's start with a video explaining this concept.

In React applications, data usually flows from the top down. Why do we care? How does this apply?

When several components in a view need to share state, you lift, or hoist, the state so that it's available to all the components that need it. Define the state in the highest component you can, so that you can pass it to any components which will need it. Let's look at a search filter as an example. This app will have two basic components - one that displays a list of data, and one that captures user input to filter the data.

We do: Build a fruit filter

Our data will be simple - a list of fruits. The app will end up looking something like this:

Fruit filter app

When building a React app, it's important to take time to define the app's structure before you start writing code. I'm going to define the components and the state I need before I write the code.

Fruit Filter data and component structure

Components

This app needs two components:

  • A List component to display the list of fruit.
    • This component needs one piece of data: the array of fruits to display.
  • An Input to capture the filter value from the user.
    • This component needs one piece of data: the current value of the filter.

State

This app needs to keep track of changes in two items:

  • The filtered list of fruits
  • The value of the filter

Component hierarchy

I have two sibling components (components at the same level of the tree/app) that need to be aware of each other's data. Specifically, the List component needs to only show the fruits that match the filter value. So I need to get data from one sibling to another. Something like this:

basic data flow needed

How to achieve this, though? Using unidrectional data flow, of course! If I create a container component to hold both the filter value and the filtered list, I can hoist the state to the container so it's available to all the children. It will then be simple to display the state in the child components. The data will flow like this:

unidirectional approach

Now that I know the components I need, the state I need, and where everything needs to be, I can start writing some code.

The fruit filter app will look like this

Before the user types, the app will look like this:

fruit filter app before typing

Afterthe user types, the app will look like this:

fruit filter app after typing

Child Components

import React, { Component } from 'react';

class List extends Component {
    render(){
        return (
            <ul>
                {/* list will go here */}
            </ul>
        )
    }
}

export default List;
import React, { Component } from 'react';

class Input extends Component {
    render(){
        return (
            <div>
                <label htmlFor="fruit-filter">Filter these Fruits: </label>
                <input type="text" id="fruit-filter" />
            </div>
        )
    }
}

export default Input;

Container component

My container will be a class with a few methods I'll use to initialize and update the state of the two child components. In the constructor, I'll initialize the state:

    state = {
      // initialize the fruit list to the full list passed in props
      fruitsToDisplay: this.props.fruits,
      // intialize the filter value to an empty string
      filterValue: ''
    }

I'll need a method to update the state when the filter value changes. This method will store the filter state, and filter the list of fruits to display. I'll pass this change handler to the filter component to react to user input.

    handleFilterChange = (e) => {
      e.preventDefault()
      const filterValue = e.target.value;
      // remove fruits that don't contain the filter value
      const filteredFruitList = this.props.fruits.filter(fruit => {
        return fruit.toLowerCase().includes(filterValue.toLowerCase())
      })
      this.setState({
          fruitsToDisplay: filteredFruitList,
          filterValue,
      })
    }

Finally, I need to render my child components.

render() {
    return (
        <div>
          <Input value={this.state.filterValue} onChange={this.handleFilterChange} />
          <List fruits={this.state.fruitsToDisplay} />
        </div>
    )
  }

The full container component looks like this:

import React, {Component} from 'react';
import Input from './Input'
import List from './List'

class FruitContainer extends Component {
    state = {
      // initialize the fruit list to the full list passed in props
      fruitsToDisplay: this.props.fruits,
      // intialize the filter value to an empty string
      filterValue: ''
    }

    handleFilterChange = (e) => {
      e.preventDefault()
      const filterValue = e.target.value;
      // remove fruits that don't contain the filter value
      const filteredFruitList = this.props.fruits.filter(fruit => {
        return fruit.toLowerCase().includes(filterValue.toLowerCase())
      })
      this.setState({
          fruitsToDisplay: filteredFruitList,
          filterValue,
      })
    }

    render() {
      return (
        <div>
          <Input value={this.state.filterValue} onChange={this.handleFilterChange} />
          <List fruits={this.state.fruitsToDisplay} />
        </div>
      )
    }

  }


export default FruitContainer

All of the data is hoisted to the top of the tree in the container, and I pass it to the child components.

Now I need to return to the children components and add the functionality to handle the data it's receiving!

Finished Children components:

import React, {Component} from 'react';

class Input extends Component {
    render(){
        return (
            <div>
                <label htmlFor="fruit-filter">Filter these Fruits: </label>
                <input type="text" value={this.props.value} onChange={this.props.onChange} id="fruit-filter" />
            </div>
        )
    }
}

export default Input;
import React, {Component} from 'react';

class List extends Component {
    render(){
        const fruitItems = this.props.fruits.map((fruit, idx)=>{
            return <li key={idx}>{fruit}</li>
        })
        return (
            <ul>
                {fruitItems}
            </ul>
        )
    }
}

export default List;

You do: Also display the fruits that do not match the filter

Add another child component to the FruitContainer, or update the List component that displays the fruits that do not match the filter value (this should be all the items that are not in the fruitsToDisplay list).

Hint: Will you need to have a new state?

Solution - Unmatching Filter

Since we are going to display the fruits that were removed, we will need a list of removed fruits in state:

  state = {
    // initialize the fruit list to the full list passed in props
    fruitsToDisplay: fruits,
    // intialize the filter value to an empty string
    filterValue: '',
    // keep track of the frutis are filtered out
    fruitsFiltered: [] // starts as an empty array -- gets filled with removed fruits
  }

Next, we need a a way to fill up the list fruits in the state with fruits that don't match in the function that handles filter changes. We can you use the same .filter() method as before, but this time we just need to reverse the boolean logic, with the logical NOT operator, !:

    // filter the reverse -- find everything that doesnt match
    const removedFruitsList = this.props.fruits.filter(fruit => {
      return !fruit.toLowerCase().includes(filterValue.toLowerCase())
    })

That was a sinch! now we just need to add the removedFruitsList to statem in the fruitsFiltered state value. Here is the entire handleFilterChange funciton:

  handleFilterChange = (e) => {
    e.preventDefault()
    const filterValue = e.target.value
    // remove fruits that don't contain the filter value
    const filteredFruitList = fruits.filter(fruit => {
      return fruit.toLowerCase().includes(filterValue.toLowerCase())
    })
    // filter the reverse -- find everything that doesnt match
    const removedFruitsList = fruits.filter(fruit => {
      return !fruit.toLowerCase().includes(filterValue.toLowerCase())
    })
    this.setState({
        fruitsToDisplay: filteredFruitList,
        filterValue,
        fruitsFiltered: removedFruitsList
    })
  }

Now, we need to pass the fruitsFiltered array to the List component, and map the list to jsx that can be rendered in an unordered list:

// rendering the List component and passing fruitsFiltered from state as a prop 'removed'
 <List  
  fruits={this.state.fruitsToDisplay} 
  removed={this.state.fruitsFiltered} 
/>

Now to render the data, the render method of the List component becomes:

  render() { 
    // update the keys, to react doesn't become angry 🤬
    const fruitItems = this.props.fruits.map((fruit, idx)=>{
      return <li key={`fruit-${idx}`}>{fruit}</li>
    })
    // map the removed fruits into a list
    const removed = this.props.removed.map((fruit, idx)=>{
      return <li key={`removed-${idx}`}>{fruit}</li>
    })
    // render the two lists
    return (
      <>
        <h3>frutis that match</h3>
        <ul>
            {fruitItems}
        </ul>

        <h3>frutis that where removed</h3>
        <ul>
            {removed}
        </ul>
      </>
    )
  }

Final Thoughts

It's important that you think through your applications before you start writing code. It's often helpful to sketch out your app, and identify:

  • the components you will need
  • the states you will need
  • which components those states will live in

Use the unidirectional data flow pattern - hoist your state toward the top of the component tree so it's available to the children that need it.