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>
);
}
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>