Maintain the context data for each child component on React router change

Viewed 193

I am beginner in React and working on React app where I am using the context to maintain the button state which can be in any one phase out of start, loading, stop.

I am passing the context to app component and have a React router to render the component on basis of route. I am rendering card component by looping through data where each card have one Button Component.

On button click of card1 the button1 should get in loading phase for 10-15 seconds depending on api response time. Once response comes it should be in stop phase. Similarly for button2 and button3 if clicked together. Now that seems to be working fine when I click on button1 and button2 instantly.

But when I click on 2 buttons together and move to another route and quickly come back I don't see my buttons to be in loading state though the api response is still pending. I should be seeing them in loading state and when response comes I should see them in start or stop phase.

I know I can use local or session storage but I don't want to due to some code restrictions.

Here is the stack blitz link : https://stackblitz.com/edit/node-3t59mt?file=src/App.js Github Link: https://github.com/mehulk05/react-context-api

Button.jsx

import React, { useContext,useEffect, useState } from 'react'
import DbContext from '../Context/sharedContext'

function Button(props) {
    console.log(props)
    const {
        requestStartDbObj,
        setRequestStartDbObj
    } = useContext(DbContext)
    const [state, setstate] = useState(props.text?.status ?? "start")
    
    useEffect(() => {
        setstate(props.text?.status??"start")
        return () => {
            
        }
    }, [state])
    console.log(requestStartDbObj)
    const start = ()=>{
        setRequestStartDbObj({id:props.status.id, status:"loading"})
        setstate("loading")
        
        setTimeout(()=>{
            setstate("stop")
            setRequestStartDbObj({id:props.status.id, status:"stop"})
        },5000)
    }
    return (
        <div>
            <button onClick={start}>{state}1</button>
        </div>
    )
}

export default Button

Card.jsx

function Card(props) {
    const {
        requestStartDbObj,
    } = useContext(DbContext)
    return (
        <div>
            <h1>{props.data.name}</h1>
            <Button status={props.data} text={requestStartDbObj} />
        </div>
    )
}

Component1.jsx

function Component1() {
  let data = [
    {
      id: 1,
      name: "card1",
      status: "start",
    },
    {
      id: 2,
      name: "card2",
      status: "start",
    },
    {
      id: 3,
      name: "card3",
      status: "start",
    },
  ];
  return (
    <div>
      <h1>Hello</h1>
      {data.map((d, i) => (
        <Card key={i} data={d} />
      ))}
    </div>
  );
}

ComponentWrapper.jsx

  <h3>Wrpper</h3>
            <Routes>
                <Route path="/" element={<Component1 />} />
                <Route path="about" element={<Component2 />} />
            </Routes>
  </div>

App.js

function App() {
  return (
    <div className="App">
       <BrowserRouter>
    
       <Link to="/">Home</Link> <br></br>
       <Link to="/about">Comp 2</Link>
       <DbProvider>
        <ComponentWrapper/>
      </DbProvider>
       </BrowserRouter>
      
    </div>
  );
}
1 Answers

The issue is that your DbProvider context isn't the source of truth as to the status, it's not the component maintaining the requestStartDbObj state. Each Button is duplicating the state locally and using its own start function. Each Button is also replacing the requestStartDbObj state of the context, so when switching back to the home path all the buttons get the same initial state value. Upon navigating away from the home path the Button component is unmounted, so the state updates on timeout are lost.

You should move the start logic to the sharedContext so DbProvider maintains control over the state updates. start should consume an id argument so it can correctly update the status for that specific object.

DbProvider

const DbProvider = (props) => {
  const [requestStartDbObj, setRequestStartDbObj] = useState({});

  const { children } = props;

  const start = (id) => {
    setRequestStartDbObj((state) => ({
      ...state,
      [id]: { status: "loading" }
    }));

    setTimeout(() => {
      setRequestStartDbObj((state) => ({
        ...state,
        [id]: { status: "stop" }
      }));
    }, 5000);
  };

  return (
    <DbContext.Provider
      value={{
        requestStartDbObj,
        start
      }}
    >
      {children}
    </DbContext.Provider>
  );
};

Card

Only pass the id prop through to Button from data prop that was passed from Component1 when mapped.

function Card({ data }) {
  return (
    <div>
      <h1>{data.name}</h1>
      <Button id={data.id} />
    </div>
  );
}

Button

Use the id prop to pass to start function provided by the context. Also use the id to access the current status.

function Button({ id }) {
  const { requestStartDbObj, start } = useContext(DbContext);

  return (
    <div>
      <button onClick={() => start(id)}>
        {requestStartDbObj[id]?.status || "start"}-{id}
      </button>
    </div>
  );
}

Edit maintain-the-context-data-for-each-child-component-on-react-router-change

Related