March 27, 2019

Articles - Tutorials

React Context vs Redux (part1)

Sometimes in our react apps we need some global variables, like user authentications, themes, shopping carts, etc. or maybe you need a state in a big tree of like a dozen of components. without a proper state management we’ll have to pass a state through lot’s of components in our component tree to reach the component we want.

In this tutorial I will compare and review two major state management solutions in react, React context and Redux.

I will create a simple app with both context api and redux, the app contains a product list and a shopping cart. you can see the Final App HERE.

this is a two part article in the first part I’ll create the app with context api, in part 2 I’ll create the same app with redux and compare these two approaches.

Using React Context api:

(complete code in this github repo)

We’re going to see how to create a global state management with context api and hooks. In this project we’ll only use functional components and hooks.

1.create context

First we need to create a context for our cart and import it in every component that we need. I’ll name it listContext:

//list_context.js
import React from 'react';

const listContext = React.createContext();

export default listContext;

2-create store

The app has a global store, which will provide all state and actions needed.

The magic of hooks

hooks were introduced in recent versions of react and finally became stable and ready to use for production in react 16.8.  in this article we’ll see how to use hooks with context to make our code more readable and more efficient.

store.js:

import React, { useState, useEffect } from 'react';
import listContext from './list_context';

//global store for our app we can create multiple contexts if needed

function Store({children}){

    // the app's initial state

    const initialState = { 
      cart:[],

      cartCount:0,
  
      addNew: addNew,
      removePd: removePd
      }

      //initiate app state with initialstates

      const [ appstate, setState ] = useState(initialState);
      
      /* pass the state as context's value. all components
      wrapped with <Store/> tag will access context's state */
      
    return(
      <listContext.Provider value={appstate}>
        {children}
      </listContext.Provider>
    )

3- add necessary functions to store

then we add functions to remove from and add products to cart and get products count to update cartCount :

////// add new product to cart and update cart count

    function addNew(pd){
        let newList = appstate.cart;
    
        const newItem = {
          count:1,
          id:pd.id,
          name:pd.name
        }
    
        const filtered = newList.filter(i =>{
          return i.id === pd.id;
        });

        /* if the product is already in the cart,
        update it's count otherwise add it to cart with 1 count */

    
        if(filtered.length > 0){
          const pos = newList.map(i => { return i.id; }).indexOf(pd.id);
          newList[pos].count += 1;
        }else{
          newList.push(newItem);
        }
        
        setState({...appstate, cart:newList, cartCount:getCartCount()});

      }
    
      ////// remove product from cart and update cart count
    
      function removePd(indx){
        const cartList = appstate.cart;
    
        cartList.splice(indx,1);
    
        setState({...appstate, cart:cartList, cartCount:getCartCount()});
      }

      ////// function to set the number of products in cart

      function getCartCount(){

        let cnt = 0;
    
        if(appstate.cart.length > 0){
    
          appstate.cart.forEach(item => {
          cnt += item.count;
          });
          
        }

        return cnt;

      }

now that the store is ready we have to wrap the components that use our global state with <store> in our case since the app isn’t big and complex and we have only one store we can wrap the whole <App/> with store tags.

index.js:

const render = 
<Store>
    <Router>
        <App/>
    </Router>
</Store>

ReactDOM.render(render, document.getElementById('root'));

4-app component and routing

We use the App component to set up our routing including a navbar and two routs that will either load Cart or List components:

App.js:

import listContext from './list_context';
import List from './components/List'
import Cart from './components/Cart'


function App() {
      // get cart count from context
      const { cartCount } = useContext(listContext);

      return ( 
        <div>
          <div className='nav'>

            <Link to='/'>
              <button>products</button>
            </Link>
  
            <Link to='/cart'>
              <button>{'cart('+cartCount+')'}</button>
            </Link>
            
          </div>
  
        <Switch>
          <Route exact path='/' component={List}/>
          <Route path='/cart' component={Cart}/>
        </Switch>
        </div>
     );
    
}

export default App;

5- list products and add to cart

This component will list the products in a list with a button to add them to cart:

components/List.js

import React, { useState, useEffect, useContext } from 'react';
import listContext from '../list_context';

function List(){

    const [list, setList] = useState([]);

    /* we imagine we load the products from a rest api
    so we use the useEffect() hook to setList as soon as
    the component is loaded */

    useEffect(()=>{
        // we assume this array was loaded from a rest api:
        const productList = [
            {name:'book', id:1}, {name:'laptop', id:2}, {name:'game console', id:3}, {name:'radio', id:4}, {name:'notenook', id:56}
        ]

        setList(productList);

    },[])

    const pdlist = list.map((i,index) => {
      return <li key={index}>id:{i.id} | product name: <strong>{i.name}</strong>  <Addbutton pd={i}/></li>
    })

    return ( 
          <ul>
            {pdlist}
          </ul>
      );
  
}

//// a button to add new product to cart

function Addbutton(props){

    const stt = useContext(listContext);

    return(
            <button onClick={()=>stt.addNew(props.pd)}>
              add to cart
            </button>    
    )
  }

  export default List;

6-shopping cart

using the same useContext hook we can both read and update our global cart state in our cart component:

import React, { useContext } from 'react';
import listContext from '../list_context';

function Cart(){

    // get shopping cart array from listcontext

    const { cart } = useContext(listContext);

    const cartlist = cart.map((i,index) => {
      return (
      <tr key={index}>
        <td>{i.id}</td>
        <td>{i.name}</td>
        <td>{'x'+i.count}</td>
        <td>{<Removebutton pd={i}/>}</td>
      </tr>
      )
    })

    if(cart.length > 0){

    return ( 

          <div style={
            {padding:'15px'}
          }>
          <table className='c'>
            
            <tr className='thead'>
              <th>ID</th>
              <th>NAME</th>
              <th>QUANTITY</th>
              <th>ACTIONS</th>
            </tr>
            {cartlist}
          </table>
          </div>
      )

        } else{
          return <p className='c'>cart is empty</p>
        }
  
}

Removebutton will remove product from cart by index of product:

/// a button to remove products from cart by index

function Removebutton(props){
  const state = useContext(listContext);
  return(
          <button onClick={()=>state.removePd(state.cart.indexOf(props.pd))}>
            remove
          </button>
  )
}

The app now functions as expected, we can list products in List component, add them to cart and list  them in Cart.js and remove them from cart.

In part 2 I’ll create the same app using redux and compare these two state management methods.

If you have any questions or ideas feel free to ask in comment section