React stale state in useEffect when using empty dependency array - javascript

I'm trying to read state after it's modified within useEffect hook. However since useEffect closes over availability being empty array, it's not updated at all causing infinite loop.
I'm intentionally using empty dependency array since I want this to happen only after
the first render, so including availability as dependency is not an option.
I need some way to read up-to-date value from availability but I don't know how to.
const [ availability, setAvailability ] = useState([])
const updateAvailability = (data, manufacturer) => {
setAvailability(availability => {
const copy = {...availability}
copy[manufacturer] = data
return copy
})
}
const parseAvailability = products => {
products.forEach(product => {
if (!availability[product.manufacturer]) { // availability is stale after updates
updateAvailability({}, product.manufacturer) // notify that data is coming soon
service.getAvailability(product.manufacturer).then(data => { // http request to fetch availability
updateAvailability(data, product.manufacturer)
})
}
})
}
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
parseAvailability(data, availRef)
})
})
},[])

Related

Update array state from out of a for each loop

I'm trying to update the state of an array from out of a forEach loop without loosing the previous state. I`m trying to archive something like the following:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
let currentState = request;
request[idx] = Object.assign(...request[idx], {answer: response.answer});
setRequests(currentState);
})
})
}
But in such a case only one response will be rendered. Any idea how to archive something like this?
The Problems
There are a couple of issues there:
You're breaking one of the fundamental rules of React state: Do not modify state directly. Doing const currentState = request; doesn't copy the object, it just makes two variables point to the same object.
You're using the version of state setter where you just pass in the update, which completely overwrites any previous outstanding state updates.
Solutions
There are (at least) two approaches here:
Do piecemeal updates as you are in that code, where earlier updates are stored in state (causing a re-render) while later updates are still in progress
or
Get all the updated information, and do a single state update (and render)
Both are valid depending on your use case.
Piecemeal Updates
For the piecemeal updates, since you're doing a bunch of state updates (which may not occur in order, depending on the vagaries of the timing of the fetch replies). Since you're updating state based on previous state (the other entries in the array), you need to use the callback version of the state setter. Then, you need to create a new array each time, and and new object within the array for the object at index idx, like this:
// *** Note I've renamed `request` to `requests` -- it's an array, it should
// use the plural, and you use `request` for an individual one later.
// I've also reversed `response` and `data`, since they were backward in the
// original. I've also added (minimal) handling of errors.
const [requests, setRequests] = useState(initialState);
const run = () => {
requests.forEach((request, idx) => {
// (It seems odd that nothing from `request` is used in the `fetch`)
fetch("/ask")
.then((response) => response.json())
.then((data) => {
setRequests((prevRequests) =>
prevRequests.map((request, index) => {
return idx === index ? { ...request, answer: data.answer } : request;
})
);
})
.catch((error) => {
// ...handle/report error...
});
});
};
map creates the new array, returning a new object for the one at index idx, or the previous unchanged ones for the others.
All At Once
The other approach is to do all the fetch calls, wait for them all to complete, and then do a single state update, like this:
// (Same naming and error-handling updates)
const run = () => {
Promise.all(requests.map((request) =>
fetch("/ask")
.then((response) => response.json())
.then((data) => {
return {...request, answer: data.answer};
})
))
.then(setRequests)
.catch((error) => {
// ...handle/report error...
});
};
You can build the new array at once and update it, something like this
const run = () => {
Promise.all(request.map((r) => fetch("/ask").then(data => data.json())))
.then((responses) => {
const newRequest = responses.map(res => ({answer: res.answer}))
setRequests(newRequest);
})
}
Using Promise.all allows you to retrieve all the json response at once, so you can build the new state in one shot.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
note: code not tested.
You problem stems from the fact that let currentState = request will keep a reference to the request at the time of making the closure. This means that it won't get updated by future calls.
You need to use the functional version of setState. Something like this:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
setRequests(currentState => {
currentState[request[idx]] = {answer: response.answer};
return {...currentState};
});
})
})
}

Resetting State in React with saved initial State

I'm trying to save State twice, so I can reset it later on, but no matter what method I try, the 'setFullTrials' won't update with the saved data. The "console.log(savedData)" shows that all the data is there, so that's definitely not the problem. Not sure where I'm going wrong.
function AllTrials({Trialsprop}) {
let [savedData, setSavedData] = useState([]);
let [fullTrials, setFullTrials] = useState([]);
useEffect(() => {
//Call the Database (GET)
fetch("/trials")
.then(res => res.json())
.then(json => {
// upon success, update trials
console.log(json);
setFullTrials(json);
setSavedData(json);
})
.catch(error => {
// upon failure, show error message
});
}, []);
const resetState = () => {
setFullTrials(savedData);
//setFullTrials((state) => ({
...state,
savedData
}), console.log(fullTrials));
// setFullTrials(savedData.map(e => e));
console.log("savedData", savedData)
}
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it will likely run before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(fullTrials)
// Whatever else we want to do after the state has been updated.
}, [fullTrials])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "fullTrials" in the example is interchangeable with whatever other state piece you're dealing with.
Check the documentation for more info.
P.S: the correct syntax for useState is with const, not let.
i.e. - const [state, setState] = useState()

Setting state on array logs empty array multiple times

I am trying to fetch data from a backend server and hold it in an array. After I have done this I want to pass the array to another
component. Although, when I try and populate the array and pass it to my component, I get multiple empty arrays passed rather than an array with data.
I first initialise the state of the array using useState()
const [data, setData] = useState([]);
I then have a function that fetches data from the backend and attempts to populate data.
useEffect(() => {
const fetchData = () => {
fetch('/data')
.then((res) => res.json())
.then((data) => {
for (const property in data) {
setDailyCases([...dailyCases].push(`${data[property]}`));
}
});
}
fetchData();
},[])
When I pass this data to another component: <DataComp data={data}, I don't get the data I was expecting.
When I console.log(props.data) this is the output:
Which is strange beacuse If I console.log() while running the data loop all the data is visible:
How can I make sure the data array is updating correctly, and when passed I get one array of all the data?
Here is the DataComp component:
const DataComp = (props) => {
console.log(props.cases)
return (
<h1>Testing</h1>
)
}
export default DataComp
Using #Fardeen Panjwani answer my component is getting the correct data, although I am now getting more outputs to the console that expected?
You're never calling setData with the fetched data as parameter.
useEffect(() => {
const fetchData = () => {
fetch('/data')
.then((res) => res.json())
.then((_data) => { // using "_data" in order to avoid clash with the state-hook
setData(_data) // <= this line is responsible for populating the "data" value.
for (const property in _data) {
setDailyCases([...dailyCases].push(`${_data[property]}`));
}
});
}
fetchData();
},[])
In order to update data's value, you need to call the setData method.

Setting a useEffect hook's dependency within`useEffect` without triggering useEffect

Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
​
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
​
I hope the above makes sense.
​
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
​
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).

React hooks: what is best practice for wrapping multiple instances of a hook with a single hook?

I have a react hook useDbReadTable for reading data from a database that accepts initial data of tablename and query. It returns an object that includes an isLoading status in addition to the data from the database.
I want to wrap this hook in a new hook that accepts initial data of an array of { tablename, query }, and returns an object with the data from the database for each table, but with the isLoading statuses consolidated into a single boolean based on logic in my new hook.
The idea is, the caller of the new hook can ask for data from a number of tables, but only has to check one status value.
My thought was to have the new hook look something like,
EDIT: Updated code (I had pasted the wrong version)
export const useDbRead = tableReads => {
let myState = {};
for (let i = 0; i < tableReads.length; ++i) {
const { tablename, query = {} } = tableReads[i];
const [{ isLoading, isDbError, dbError, data }] = useDbReadTable(tablename, query);
myState = { ...myState, [tablename]: { isLoading, isDbError, dbError, data }};
}
const finalState = {
...myState,
isLoading: Object.values(myState).reduce((acc, t) => acc || t.isLoading, false),
};
return [finalState];
};
However, eslint gives me this error on my useDbReadTable call:
React Hook "useDbReadTable" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render. react-hooks/rules-of-hooks
And Rules for Hooks says,
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)
After reading the rule and the explanation, it seems the only issue is making sure the hooks are called in the same order on all re-renders. As long as I ensure the list of tables I pass in to my new hook never changes, shouldn't my new hook work fine (as my initial tests indicate)? Or am I missing something?
More importantly, is there a better idea how to implement this, that doesn't violate the Rules of Hooks?
Edit2: in case its helpful, here's useDbReadTable. Note that it includes more functionality than I mention in my question, since I wanted to keep the question as simple as possible. My question is whether my useDbRead is a good solution, or is there a good way to do it without violating the Rules of Hooks?
export const useDbReadTable = (initialTableName, initialQuery = {}, initialData = []) => {
const dbChangeFlag = useSelector(({appState}) => appState.dbChangeFlag);
const [tableName, setTableName] = useState(initialTableName);
const [query, setQuery] = useState(initialQuery);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isDbError: false,
dbError: {},
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: dataFetch.FETCH_INIT });
try {
const result = Array.isArray(query) ?
await db[tableName].batchGet(query) // query is an array of Ids
:
await db[tableName].find(query);
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_SUCCESS, payload: result });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_FAILURE, payload: error });
}
}
};
fetchData().then(); // .then() gets rid of eslint warning
return () => {
didCancel = true;
};
}, [query, tableName, dbChangeFlag]);
return [state, setQuery, setTableName];
};
You can probably avoid using the useDbReadSingle by making useDbRead itself array aware. Something like:
export const useDbRead = tableReads => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const doIt = async () => {
// you would also need to handle the error case, but you get the idea
const data = await Promise.all(
tableReads.map(tr => {
return mydbfn(tr);
})
);
setLoading(false);
};
doIt();
}, [tableReads]);
return { loading, data };
};
When you need to use it for single table read, just call this with a array that has single element.
const {loading, data: [d]} = useDbRead([mytableread])

Categories