Search code examples
reactjsreact-hooksdata-synchronization

React Initializing and synchronizing state with backend


I wonder what is the best way/pattern to initialize state and keep it synced with the server. I've read and tried a lot for the last couple of days but haven't found anything that solves my question.

The example is really simple. There is a state - it's a number in the example for the sake of simplicity, although in real life it would be an object - that I need to retrieve from the server. Once retrieved, I want it to be synchronized with the server. The getValueFromServer is a mock that returns a random value after waiting a random amount of time in a setTimeout.

So, to initialize my state I use a useEffect on an empty array as a dependency, and to keep it synched I use a useEffect with the state as a dependency.

The problem is that it is trying to save an undefined value. The log is as follows.

1 -> Initializing for first time

2 -> API call to save in server value: undefined

3 -> API call to save in server value: 6.026930847574949

What I get:

1: Runs on mounting as expected.

2: This one I didn't expect. I guess it is triggered because of the "useState".

3: Runs once we get the response from the server. Kind of obvious but a pain in the ass, because why on earth would I want to save this.

What would the best approach be here? Using something like a "isInitialized" flag in the useEffect with dependency feels kind of hacked and not professional.

Code below and you can find it working here too: https://codesandbox.io/s/optimistic-rgb-uce9f

import React, { useState, useEffect } from "react";
import { getValueFromServer } from "./api";

export default function App() {
  const [value, setValue] = useState();

  useEffect(() => {
    async function initialize() {
      console.log("Initializing for first time");
      let serverValue = await getValueFromServer();
      setValue(serverValue);
    }
    initialize();
  }, []);

  useEffect(() => {
    console.log("API call to save in server value: ", value);
  }, [value]);

  const handleClick = () => {
    setValue(value + 1);
  };

  return (
    <div className="App">
      <h1>Value: {value}</h1>
      <button onClick={handleClick}>Add 1 to value</button>
    </div>
  );
}

Solution

  • What would the best approach be here? Using something like a "isInitialized" flag in the useEffect with dependency feels kind of hacked and not professional.

    You can either use a flag or initialize object with default value in useState

    const { Fragment, useState, useEffect } = React;
    
    const getValueFromServer = () => new Promise((resolve, reject) => setTimeout(() => resolve(Math.random())), 1000)
    
    const App = () => {
      const [value, setValue] = useState(null);
      const [isLoading, setLoading] = useState(true);
    
      useEffect(() => {
        let isUnmounted = false;
      
        getValueFromServer().then(serverValue => {
          console.log("Initializing for first time");
          if(isUnmounted) {
            return;
          }
          setValue(serverValue);
          setLoading(false);
        })
        
        return () => {
          isUnmounted = true;
        }
      }, []);
    
      useEffect(() => {
        if(!value) {
          return () => {}
        }
      
        console.log("API call to save in server value: ", value);
        setTimeout(() => setLoading(false), 50);
      }, [value]);
    
      const handleClick = () => {
        setLoading(true);
        setValue(value + 1);
      };
    
      return <div className="App">
        {isLoading ? <Fragment>
          <span>Loading...</span>
        </Fragment> : <Fragment>
          <h1>Value: {value}</h1>
          <button onClick={handleClick}>Add 1 to value</button>
        </Fragment>}
      </div>
    }
    
    ReactDOM.render(
        <App />,
        document.getElementById('root')
      );
    <script src="https://unpkg.com/react/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.10.1/polyfill.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    <div id="root"></div>