Reputation: 1679
I'm building a custom hook to manage the data coming from a fetch/axios/promise/whateveryouwant function, this hooks allow me to update the data whenever I want using an update function that I return at the end of my hook.
So for now I have two states inside my hooks:
const [data, setData] = useState<T>(defaultValue); // the data coming from the fetch, T is defined by the user of the hook
const [query, setQuery] = useState<Array<{ key: string; value: string }>>([]); // array of query option for the URL
I wanted to implement a dynamic way to add query string to the URL, so I used the query state to have an array of object representing what I wanted but I cannot access the value of the query inside of the update function when calling from outside. Here is the hooks, I highlited the important part :
export default function useFetchV2<T>(
defaultValue: T,
getData: (queryString?: string) => Promise<T>
): {
update: () => void;
data: T;
updateQueryValue: (key: string, value: string) => void; // update, data, updateQueryValue are the return value of the hook.
} {
const [data, setData] = useState<T>(defaultValue);
const [query, setQuery] = useState<Array<{ key: string; value: string }>>([]); // THE STATE I WANT TO ACCESS
console.log(query, data); // Here the state is updating well and everything seems fine
const update = useCallback((): void => {
let queryString;
console.log(query, data);
// But here is the problem, query and data are both at their default value. The state inside the hooks is correct but not here.
if (query.length > 0) { // query.length always at 0
queryString = _.reduce(
query,
(acc, el) => {
return `${acc}${el.key}=${el.value.toString()}`;
},
'?'
);
console.log(queryString);
}
getData(queryString).then(res => setData(res));
}, [data, getData, query]);
const updateQueryValue = useCallback(
(key: string, value: string): void => {
const index = query.findIndex(el => el.key === key);
if (index !== -1) {
if (!value) {
const toto = [...query];
_.pullAt(toto, index);
setQuery(toto);
} else {
setQuery(prev =>
prev.map(el => {
if (el.key === key) {
return { key, value };
}
return el;
})
);
}
} else {
console.log(key, value); // everything is logging well here, good key and value
setQuery([...query, { key, value }]); // setQuery correctly update the state
}
update();
},
[query, update]
);
useEffect(() => {
update();
}, []);
return { update, data, updateQueryValue };
}
It might be the way I'm exporting the function, I'm still not used to the scope.
I called updateQueryValue from a component. The function is called, state is changed, but the update function can't see the difference.
Upvotes: 1
Views: 911
Reputation: 44880
The code is calling setQuery
and then immediately calling update
and expecting query
to be updated value, but it will always be the current value of query
. Getting that to happen is the use case of useEffect
.
} else {
console.log(key, value); // everything is logging well here, good key and value
setQuery([...query, { key, value }]); // setQuery correctly update the state
}
// This will *always* see the current value of `query`, not the one that
// was just created in the `else` block above.
update();
update
after query
is updated, which is exactly what useEffect
is for.const updateQueryValue = useCallback(
(key: string, value: string): void => {
const index = query.findIndex(el => el.key === key);
if (index !== -1) {
if (!value) {
const toto = [...query];
_.pullAt(toto, index);
setQuery(toto);
} else {
setQuery(prev =>
prev.map(el => {
if (el.key === key) {
return { key, value };
}
return el;
})
);
}
} else {
setQuery([...query, { key, value }]); // setQuery correctly update the state
}
// This call isn't needed, it will always "see" the old value of
// `query`
// update();
},
[query, update]
);
// Call `update` once when this custom hook is first called as well as
// whenever `query` changes.
useEffect(() => {
update();
}, [query]); // <-- this is new, add `query` as a dependency
useCallback
is only needed if something is relying on the identity of the function to make decisions; e.g. if a component receives the function as a prop, it checks whether the prop changes to decide whether to re-render. update
in this case does not need a useCallback
and might make this hook slightly harder to debug.function update() {
let queryString;
if (query.length > 0) { // query.length always at 0
queryString = _.reduce(
query,
(acc, el) => {
return `${acc}${el.key}=${el.value.toString()}`;
},
'?'
);
console.log(queryString);
}
getData(queryString).then(res => setData(res));
});
Upvotes: 1