Search code examples
reactjsreduxuse-effectuse-state

Set the local state using useEffect on Redux prop change


I am new to React Redux and I am trying to setState on a prop change in Redux using a useEffect hook.

I have the following code:

const DeploymentOverview = ({diagram, doSetDiagram}) => {
    const { diagram_id } = useParams()
    const [instances, setinstances] = useState(null)
    const [error, seterror] = useState([false, ''])
    
    useEffect(() => {
        GetDiagram(diagram_id).then(d => doSetDiagram(d)).catch(err => seterror([true, err]))
    }, [doSetDiagram])

    useEffect(() => {
        if (diagram) {
            if (diagram.instances) {
                let statusList = []
                diagram.instances.forEach(instance => {
                    InstanceStatus(instance.key)
                    .then(status => statusList.push(status))
                    .catch(err => seterror([true, err]))
                });
                setinstances(statusList)
            }
        }
    }, [diagram])

    return (
        <Container>
            {error[0] ? <Row><Col><Alert variant='danger'>{error[1]}</Alert></Col></Row> : null}
            {instances ? 
            <>
                <Row>
                    <Col>
                        <h1>Deployment of diagram X</h1>
                        <p>There are currently {instances.length} instances associated to this deployment.</p>
                    </Col>
                </Row>
                <Button onClick={setinstances(null)}><FcSynchronize/> refresh status</Button>
                <Table striped bordered hover>
                        <thead>
                            <tr>
                                <th>Status</th>
                                <th>Instance ID</th>
                                <th>Workflow</th>
                                <th>Workflow version</th>
                                <th>Jobs amount</th>
                                <th>Started</th>
                                <th>Ended</th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody>
                        {instances.map(instance => 
                            <tr>
                                <td>{ <StatusIcon status={instance.status}/> }</td>
                                <td>{instance.id}</td>
                                {/* <td>{instance.workflow.name}</td>
                                <td>{instance.workflow.version}</td> */}
                                {/* <td>{instance.jobs.length}</td> */}
                                <td>{instance.start}</td>
                                <td>{instance.end}</td>
                                <td><a href='/'>Details</a></td>
                            </tr>
                        )}
                    </tbody>
                </Table>
                </> 
                
        : <Loader />}
            
        </Container>
    )
}

const mapStateToProps = state => ({
    diagram: state.drawer.diagram
})

const mapDispatchToProps = {
    doSetDiagram: setDiagram
}

export default connect(mapStateToProps, mapDispatchToProps)(DeploymentOverview)

What I want in the first useEffect is to set de Redux state of diagram (this works), then I have a other useEffect hook that will get a list from one of the diagrams attributes named instances next I loop over those instances and do a fetch to get the status of that instance and add this status to the statusList. Lastly I set the instances state using setinstances(statusList)

So now I expect the list of statusresults being set into instances and this is the case (also working?). But then the value is changed back to the initial value null...

console

In my console it's first shows null (ok, initial value), then the list (yes!) but then null again (huh?). I read on the internet and useEffect docs that the useEffect runs after every render, but I still don't understand why instances is set and then put back to it's initial state.

I am very curious what I am doing wrong and how I can fix this.


Solution

  • If you have multiple async operations you can use Promise.all:

    useEffect(() => {
      if (diagram) {
        if (diagram.instances) {
          Promise.all(
            diagram.instances.map((instance) =>
              InstanceStatus(instance.key)
            )
          )
            .then((instances) => setInstances(instances))
            .catch((err) => setError([true, err]));
        }
      }
    }, [diagram]);
    

    Here is a working example:

    const InstanceStatus = (num) => Promise.resolve(num + 5);
    const useEffect = React.useEffect;
    const App = ({ diagram }) => {
      const [instances, setInstances] = React.useState(null);
      const [error, setError] = React.useState([false, '']);
      //the exact same code from my answer:
      useEffect(() => {
        if (diagram) {
          if (diagram.instances) {
            Promise.all(
              diagram.instances.map((instance) =>
                InstanceStatus(instance.key)
              )
            )
              .then((instances) => setInstances(instances))
              .catch((err) => setError([true, err]));
          }
        }
      }, [diagram]);
      return (
        <pre>{JSON.stringify(instances, 2, undefined)}</pre>
      );
    };
    const diagram = {
      instances: [{ key: 1 }, { key: 2 }, { key: 3 }],
    };
    
    ReactDOM.render(
      <App diagram={diagram} />,
      document.getElementById('root')
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    What you did wrong is the following:

    diagram.instances.forEach(instance => {
      InstanceStatus(instance.key)//this is async
      //this executes later when the promise resolves
      //mutating status after it has been set does not 
      //re render your component
      .then(status => statusList.push(status))
      .catch(err => seterror([true, err]))
    });
    //this executes immediately so statusList is empty
    setinstances(statusList)