I noticed a behavior I cant understand how to solve, I never had it while I was writing react class based using this.setState but I get it while using useState hook.
for example:
const Main = () => {
const [values, setValues] = useState({val1: '', val2: '', val3: ''})
const func1 = async () => {
await setValues({
...values,
val1: '111111111'
});
await func2();
}
const func2 = async () => {
result = (fetch some data);
await setValues({
...values,
val2: result
});
}
};
now if you run func1, val1 will be changed but as soon as func2 finishes and we setValues the second time val1 will get overwritten and val2 will contain value.
what am I doing wrong and how can I fix this ?
Thanks in Advance!
Edit:
when using Hooks I cant see what is the acctual anme of the value entered in the React Chrome Dev tool.
is there a way to fix this ?
when I was having one useState containing an object I could see the titles of each object key... now its hidden -_-
You're spreading the state before updating the correspondent property, try to chunk your state
const Main = () => {
const [value1, setValue1] = useState(null)
const [value2, setValue2] = useState(null)
const func1 = async () => {
setValue1('foo')
await func2();
}
const func2 = async () => {
result = (fetch some data);
await setValue2('foo')
}
};
Here is what is happening
setValues is called changing val1 (state isn't updated yet)
setValues is called again changing val2 and spreading the rest
By the time setValues spreads values val1 still holds it's initial value, overwriting the first setValues call. Remember, changes in state are reflected asynchronously
React useState also takes a functional updater to update your component state similarly to how class-based component's setState works.
Note
Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
So in your code you could update as follows:
const Main = () => {
const [values, setValues] = useState({val1: '', val2: '', val3: ''});
const func1 = async () => {
await setValues(prevState => ({
...prevState,
val1: '111111111'
}));
await func2();
}
const func2 = async () => {
result = (fetch some data);
await setValues(prevState => ({
...prevState,
val2: result
}));
}
};
I haven't tested it yet but maybe it could work as an alternative.
Sometimes, when we need to get the value in the state, we usually do the spread operator within the state but it doesn't guarantees that the value is the correct one. For those cases, we better call setState with a callback which takes the previous value from state. So, you can use something like this:
setValues(prev => ({...prev, val1:'11111'}));
The behaviour of the code becomes clear once we note that values variable in func2 is clousre on values at outer scope, which is really a copy of state taken at the time useState was called.
So what your spreading in func2 is is stale copy of your state.
One way of correcting this would be to use functional update
setValues((values) => ({...values, val2: result}));
This will make sure that you are using updated value of the state.
Related
I use a lot of firestore snapshots in my react native application. I am also using React hooks. The code looks something like this:
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// When the component initially loads, add all the loaded data to state.
// When data changes on firestore, we receive that update here in this
// callback and then update the UI based on current state
});;
}, []);
At first I assumed useState would be the best hook to store and update the UI. However, based on the way my useEffect hook is set up with an empty dependency array, when the snapshot callback gets fired with updated data and I try to modify the current state with the new changes, the current state is undefined. I believe this is because of a closure. I am able to get around it using useRef with a forceUpdate() like so:
const dataRef = useRef(initialData);
const [, updateState] = React.useState();
const forceUpdate = useCallback(() => updateState({}), []);
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// if snapshot data is added
dataRef.current.push(newData)
forceUpdate()
// if snapshot data is updated
dataRef.current.find(e => some condition) = updatedData
forceUpdate()
});;
}, []);
return(
// JSX that uses dataRef.current directly
)
My question is am I doing this correct by using useRef along with a forceUpdate instead of useState in a different way? It doesn't seem right that I'm having to update a useRef hook and call forceUpdate() all over my app. When trying useState I tried adding the state variable to the dependency array but ended up with an infinite loop. I only want the snapshot function to be initialized once and the stateful data in the component to be updated over time as things change on the backend (which fires in the onSnapshot callback).
It would be better if you combine useEffect and useState. UseEffect will setup and detach the listener, useState can just be responsible for the data you need.
const [data, setData] = useState([]);
useEffect(() => {
const unsubscribe = someFirestoreAPICall().onSnapshot(snap => {
const data = snap.docs.map(doc => doc.data())
this.setData(data)
});
//remember to unsubscribe from your realtime listener on unmount or you will create a memory leak
return () => unsubscribe()
}, []);
Then you can just reference "data" from the useState hook in your app.
A simple useEffect worked for me, i don't need to create a helper function or anything of sorts,
useEffect(() => {
const colRef = collection(db, "data")
//real time update
onSnapshot(colRef, (snapshot) => {
snapshot.docs.forEach((doc) => {
setTestData((prev) => [...prev, doc.data()])
// console.log("onsnapshot", doc.data());
})
})
}, [])
I found that inside of the onSnapshot() method I was unable to access state(e.g. if I console.log(state) I would get an empty value.
Creating a helper function worked for, but I'm not sure if this is hack-y solution or not but something like:
[state, setState] = useState([])
stateHelperFunction = () => {
//update state here
setState()
}
firestoreAPICall.onSnapshot(snapshot => {
stateHelperFunction(doc.data())
})
use can get the currentState using callback on set hook
const [state, setState] = useState([]);
firestoreAPICall.onSnapshot(snapshot => {
setState(prevState => { prevState.push(doc.data()) return prevState; })
})
prevState will have Current State Value
I need to have 2 different functions that update 2 different components only once in the beginning. Hence, I'm using useEffect. The code is as follows
const loadCategories = () => {
getCategories().then((c) => setValues({ ...values, categories: c.data }));
}
const loadStores = () => {
getStores().then((c) => setValues({ ...values, stores: c.data }));
}
useEffect(() => {
loadStores();
loadCategories();
}, []);
Both the functions are setting the values of the dropdown elements
The problem is though both functions are exectued, only loadCategories() function logic is reflected in the UI. How to make both functions logic reflect in the UI?
first better practice to add those function in useEffect or to wrap them in useCallback hook.
second both or your function are promises so each may not resolve at same time and when you trying to update state values will keep it initial value that why your first function is not reflecting in the ui instead use setState callback to get the previous state like this :
useEffect(() => {
const loadCategories = () => {
getCategories().then((c) => setValues(prevState=>({ ...prevState, categories: c.data })));
}
const loadStores = () => {
getStores().then((c) => setValues(prevState=>({ ...prevState, stores: c.data })));
}
loadStores();
loadCategories();
}, []);
Promise and useEffect can be challenging as the component might dismount before you promise is full-filled.
Here is a solution which works quite well:
useEffect(() => {
let isRunning = true;
Promise.all([
getCategories(),
getStores()
]).then(([stores, categories]) => {
// Stop if component was unmounted:
if (!isRunning) { return }
// Do anything you like with your lazy load values:
console.log(stores, categories)
});
return () => {
isRunning = false;
}
}, []);
Wait for both promises to resolve and then use the data from both to update your state, combined in whatever way you see fit.
useEffect(() => {
Promise.all([getCategories(), getStores()]).then(([categories, stores]) => {
setValues({categories, stores})
});
}, []);
The problem with what you had before (as you experienced) is that values is always the value at the point at which useState was run on this render.
If you really want to do the updates separately, than you can look into useReducer: https://reactjs.org/docs/hooks-reference.html#usereducer
You are lying to React about dependencies. Both your functions depend on the values state variable. That's also why it does not work: When the hooks get run, values gets closured, then when setValues runs values changes (gets set to a new object), however inside the closure it is still referencing the old values.
You can easily resolve that by passing a callback to setValues, this way you do not have a dependency to the outside and the values update is atomic:
setValues(values => ({ ...values, stores: c.data }));
Can someone please explain why the value of key within the arrow function is undefined:
// in parent component
const Parent = () => {
const [key, setKey] = useState<string>();
// this contains an expensive function we only wish to execute once on first load
useEffect(() => {
// has some promise that will call within a `then()`
setKey(someVal);
}, []};
// within render
< MyComponent key={key}/>
}
// within child component
interface Props {
key: string;
}
const MyComponent = ({key}: Props) => {
// this works, I can see the correct value `someVal`
console.log("value when rendered: " + key);
const callback = () => {
// this isn't working, key is undefined
console.log("value within callback: " + key);
}
// within render, when something calls callback(), key is undefined, why?
}
I can see that key has a value when the render is called, but key is undefined. I've tried using let callback = instead of const, but no luck. How do I access key please?
In React, key is a reserved prop name.
[...] attempting to access this.props.key from a component (i.e., the render function or propTypes) is not defined
https://reactjs.org/warnings/special-props.html
Which is probably the reason why it doesn't work in subsequent renders — I'm surprised that it worked in the first render at all!
This works fine:
// https://codepen.io/d4rek/pen/PoZRWQw
import { nanoid } from 'https://cdn.jsdelivr.net/npm/nanoid/nanoid.js'
const Child = ({ id }) => {
console.log(`within component: ${id}`)
const cb = () => console.log(`in callback: ${id}`)
return <button onClick={cb}>Click me</button>
}
const Parent = () => {
const [id, setId] = React.useState(null)
React.useEffect(() => {
setId(nanoid(6))
}, [])
return (<Child id={id} />)
}
ReactDOM.render(<Parent />, document.body)
import React, { useCallback } from 'react';
const callback = useCallback(() => {
// this isn't working, key is undefined
console.log("value within callback: " + key);
}, [key]);
The reason why yours is not working: props are bound to this but the callback as you defined it has its own scope and hence has its own this. So you need to provide the value to that scope. You can do it by using local state of the component. Since there are some nice hooks in React to make that easy you should use them to memorize the callback:
React.useCallback(() =>{console.log(key);}, [key]);
Note the dependency array that updates the callback when key changes. Here the scoping is fine.
I have a simple React component (Hooks):
// inside React component
import { someExternalFunction } from "functions"
const [value, setValue] = useState(0)
const handleChange = () => {
someExternalFunction(value, setValue)
}
// outside of React component
const someExternalFunction = (value, setValue) => {
console.log(value) // 0
// testing "set" method
setValue(100) // working
// "set" is async, so lets wait
// testing "get" method
setTimeout(() => console.log(value), 5000) // 0
// not working
}
PROBLEM: "value"/state is always the same, it is captured at the moment when state is passed to a function.
QUESTION: How to access the latest state in external function?
CLARIFICATION: Of course it is not working with the "value"/state, I just tried to illustrate the problem that I'm facing with preferred method for providing the latest state to the function (trouh the direct reference) .
Value is primitive type, not reference type, it was pass into function as a copied value, of cause it is never changed. And setTimeout always access a copied value.
As long as you don't trigger "handleChange" again, the function won't use the new value. Your state is a number so it will not pass a reference but rather the value itself (in this case 0). When you call setValue, the state is updated but your timeOut callback still uses the stale value that was passed when you triggered "handleChange".
If you want to react to a state change a useEffect hook would be better.
You can also simply put a console.log(value) below the line where you define the state, to check if it changes.
// Update:
const [value, setValue] = React.useState(0);
const handleChange = () => {
setValue(100);
}
React.useEffect(() => console.log(value), [value]);
Or if you want to move that logic to a reusable hook:
// useExternalFunction.js
export useExternalFunction = () => {
const [value, setValue] = React.useState(0);
const handleChange = () => {
setValue(100);
}
React.useEffect(() => console.log(value), [value]);
return {value, handleChange};
}
// component
const {value, handleChange} = useExternalFunction();
return <button onClick={handleChange}>{value}</button>;
I try to use useState with an array of objects as a value. Something like this:
const [state, setState] = useState(arg)
Arg is the result of function which returns an array like this:
[{a:1, b:2}, {a:3, b:4}]
But when I try to use it, my state is empty and nothing happens. How to use an arg in useState?
I see another similar questions, but these solutions doesn't work. Or maybe I don't understand.
//take users list from DB
const dispatch = useDispatch()
const items = useSelector((state) => state.users.list)
useEffect(() => {
dispatch(funcThatGetUsersList)
}, [dispatch])
//use items for handler
const [list, setList] = useState([])
function checkboxHandler = (event) => {
do smth with setList(list)
}
Added a little piece of code
If you use lazy initial state, the function will be evaluated only once.
If it returns different values in subsequent renders, those other values will be ignored.
const condition = false
const getArg = () => condition ? arg : []
useState(getArg) // getArg() will be called only once
you cannot use useState directly. Perhaps my solution will help. I passed the value with useEffect. such as
useEffect(() => {
if (list)
setList(yourFunc)
)
}, [yourFunc])