function App(){
const [listState, setListState] = useState([]);
let connectWebsocket = () => {
let ws = new WebSocket(`ws://${window.location.host}/ws`);
ws.onmessage = (event) => {
let data = JSON.parse(event.data);
setListState([...listState, data]);
};
}
useEffect(() => {
console.log(listState); // Problem: This always logs empty array.
}, [listState]);
useEffect(() => {
connectWebsocket();
}, []);
return (
<div>
</div>
);
}
When I update state in nested functions, the state is updated to an initial value.
How to fix this problem?
Related
I have some functional component. Inside component I get value from redux store (I am using redux-toolkit). Also I have handler inside this component.
The value of variable from store set after request to api via RTK Query. So, the variable first has a default value, and then changes to value from the api.
Problem:
The value of variable from redux store doesn't updated inside handler.
const SomeContainer = () => {
const dispatch = useDispatch();
const variableFromStore = useSelector(someSelectors.variableFromStore);
console.log(variableFromStore) **// correct value (updated)**
const handleSomeAction = () => {
console.log(variableFromStore) **// default value of init store (not updated)**
};
return <SomeComponent onSomeAction={handleSomeAction} />;
};
SomeComponent
const SomeComponent = (props) => {
const { list, onSomeAction } = props;
const moreRef = useRef(null);
const loadMore = () => {
if (moreRef.current) {
const scrollMorePosition = moreRef.current.getBoundingClientRect().bottom;
if (scrollMorePosition <= window.innerHeight) {
onSomeAction(); // Call handler from Container
}
}
};
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
};
}, []);
return (
...
);
};
How is it possible? What do I not understand?)
The problem is you're unintentionally creating a closure around the original version of handleSomeAction:
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
}
}, []);
The dependencies array here is empty, which means this effect only runs the first time that your component mounts, hence capturing the value of loadMore at the time the component mounts (which itself captures the value of onSomeAction at the time the component mounts).
The "easy fix" is to specify loadMore as a dependency for your effect:
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
}
}, [loadMore]);
BUT! This will now create a new problem - handleSomeAction is recreated on every render, so your effect will now also run on every render!
So, without knowing more details about what you're actually trying to do, I'd use a ref to store a reference to the onSomeAction, and the inline the loadMore into your effect:
// A simple custom hook that updates a ref to whatever the latest value was passed
const useLatest = (value) => {
const ref = useRef();
ref.current = value;
return ref;
}
const SomeComponent = (props) => {
const { list, onSomeAction } = props;
const moreRef = useRef(null);
const onSomeActionRef = useLatest(onSomeAction);
useEffect(() => {
const loadMore = () => {
if (!moreRef.current) return;
const scrollMorePosition = moreRef.current.getBoundingClientRect().bottom;
if (scrollMorePosition <= window.innerHeight) {
onSomeActionRef.current();
}
}
window.addEventListener('scroll', loadMore);
return () => window.removeEventListener('scroll', loadMore);
}, []);
return (
...
);
};
there is something strange happening with my code. My variable data (useState) is randomly empty when I call my callback when onpopstate event is fired.
I have 2 components and 1 hook used like that:
const Parent = props => {
const {downloadData} = useData();
const [data, setData] = useState([]);
const [filteredData, setFilteredData] = useState();
const loadData = async () => setData(await downloadData());
useEffect(() => {
loadData();
}, []);
return <FilterPage data={data} onDataChange={data => setFilteredData(data)} />
}
const FilterPage = ({data, onDataChange} => {
const {saveHistoryData} = useHistoryState('filter', null, () => {
updateFilters();
});
const filter = (filterData, saveHistory = true) => {
let r = data; // data is randomly empty here
...
if(saveHistory)saveHistoryData(filterData);
onDataChange(r);
}
});
// my hook
const useHistoryState = (name, _data, callback) => {
const getHistoryData = () => {
const params = new URLSearchParams(window.location.search);
try{
return JSON.parse(params.get(name));
}catch(err){
return null;
}
}
const saveHistoryData = (data) => {
const params = new URLSearchParams(window.location.search);
params.set(name, JSON.stringify(data || _data));
window.history.pushState(null, '', window.location.pathname + '?' + params.toString());
}
const removeHistoryData = () => {
const params = new URLSearchParams(window.location.search);
params.delete(name);
window.history.pushState(null, '', window.location.pathname + '?' + params.toString());
}
const watchCallback = () => {
callback(getHistoryData());
};
useEffect(() => {
let d = getHistoryData();
if(d)watchCallback();
window.addEventListener('popstate', watchCallback);
return () => window.removeEventListener('popstate', watchCallback);
}, []);
return {getHistoryData, saveHistoryData, removeHistoryData};
}
Any suggestions please
Edit
I'm sorry is not the entire code, just a draft. I download the data using async function. The data is loading fine but is empty only if we call the callback from the hook.
You need to use setData to populate data
First of all you are not calling setData() anywhere.
You are using data but not setData and you are using setFilteredData but not filteredData.
Furthermore it doesn't look like updateFilters() exist within FilterPage.
You are passing onDataChange to <Filterpage> but you are not using the property, only ({data}) which explains why it's empty. You might want to update the FilterPage signature: const FilterPage = ({data, onDataChange}) => {} and use the onDataChange
If I used useCallback for the methods of my custom hook, should I also memoize the returned object? I would assume that it would not create a new object everytime since it's composed with memoized methods and primitives.
export const useToggle = (args: UseToggleProps) => {
const initialState=
args.initialState !== undefined ? args.initialState : false
const [active, setActive] = useState(initialState)
const toggleOff = useCallback(() => {
setActive(false)
}, [])
const toggleOn = useCallback(() => {
setActive(true)
}, [])
const toggle = useCallback(() => {
setActive((active) => !active)
}, [])
// Should this also be memoized with useMemo?
return {
active,
toggle,
toggleOff,
toggleOn
}
}
You don't need to memoize the returned object unless you are passing the Object directly to function of useEffect in which case reference will fail
You don't need to add an extra layer of memoization over useCallback if you use it like this:
const Comp = () => {
const { toggleOn, toggleOff } = useToggle();
useEffect(() => {
console.log('Something here')
}, [toggleOn]);
return (
<Child toggleOn={toggleOn} toggleOff={toggleOff} />
)
}
However the usages like below code will need memoization of the returned object
const Comp = () => {
const toggle = useToggle();
useEffect(() => {
console.log('Something here')
}, [toggle]);
return (
<Child toggleHandlers={toggle} />
)
}
I have a User component and I want to make an api call to update the inactive state.
The problem that I´m having is that the useEffect is being called when the component receives props. That means it will make the call when component renders for the first time. What´s the pattern to avoid this and only make the call if inactive state changes?
My initial try was to remove user from the dependencies array. But I need to get the user.id to pass it to the service.
What´s the pattern to avoid making these unnecessary calls?
const User = (props) => {
const { user } = props
const [inactive, setInactive] = useState(user.inactive);
useEffect(() => {
const abortController = new AbortController();
const data = { inactive: Number(inactive) }
updateUserRecordInDatabase(user.id, data);
return () => {
abortController.abort();
};
}, [inactive, user])
return (
<button onClick={ () => setInactive(!inactive) } />toggle { user.name } state</button>
)
}
You can try:
const User = (props) => {
const { user } = props
const [inactive, setInactive] = useState(user.inactive);
const [numUpdate, setNumUpdate] = useState(0);
const {id} = user //You get id of use here
useEffect(() => {
if(numUpdate === 0){
// This is first time of useEffect
setNumUpdate(numUpdate + 1); // Only update when first time of useEffect
}
const abortController = new AbortController();
const data = { inactive: Number(inactive) }
updateUserRecordInDatabase(id, data);
return () => {
abortController.abort();
};
}, [inactive, id])
return (
<button onClick={ () => setInactive(!inactive) } />toggle { user.name } state</button>
)
}
I'm learning to React Hooks.
And I'm struggling initialize data that I fetched from a server using a custom hook.
I think I'm using hooks wrong.
My code is below.
const useFetchLocation = () => {
const [currentLocation, setCurrentLocation] = useState([]);
const getCurrentLocation = (ignore) => {
...
};
useEffect(() => {
let ignore = false;
getCurrentLocation(ignore);
return () => { ignore = true; }
}, []);
return {currentLocation};
};
const useFetch = (coords) => {
console.log(coords);
const [stores, setStores] = useState([]);
const fetchData = (coords, ignore) => {
axios.get(`${URL}`)
.then(res => {
if (!ignore) {
setStores(res.data.results);
}
})
.catch(e => {
console.log(e);
});
};
useEffect(() => {
let ignore = false;
fetchData(ignore);
return () => {
ignore = true;
};
}, [coords]);
return {stores};
}
const App = () => {
const {currentLocation} = useFetchLocation();
const {stores} = useFetch(currentLocation); // it doesn't know what currentLocation is.
...
Obviously, it doesn't work synchronously.
However, I believe there's the correct way to do so.
In this case, what should I do?
I would appreciate if you give me any ideas.
Thank you.
Not sure what all the ignore variables are about, but you can just check in your effect if coords is set. Only when coords is set you should make the axios request.
const useFetchLocation = () => {
// Start out with null instead of an empty array, this makes is easier to check later on
const [currentLocation, setCurrentLocation] = useState(null);
const getCurrentLocation = () => {
// Somehow figure out the current location and store it in the state
setTimeout(() => {
setCurrentLocation({ lat: 1, lng: 2 });
}, 500);
};
useEffect(() => {
getCurrentLocation();
}, []);
return { currentLocation };
};
const useFetch = coords => {
const [stores, setStores] = useState([]);
const fetchData = coords => {
console.log("make some HTTP request using coords:", coords);
setTimeout(() => {
console.log("pretending to receive data");
setStores([{ id: 1, name: "Store 1" }]);
}, 500);
};
useEffect(() => {
/*
* When the location is set from useFetchLocation the useFetch code is
* also triggered again. The first time coords is null so the fetchData code
* will not be executed. Then, when the coords is set to an actual object
* containing coordinates, the fetchData code will execute.
*/
if (coords) {
fetchData(coords);
}
}, [coords]);
return { stores };
};
function App() {
const { currentLocation } = useFetchLocation();
const { stores } = useFetch(currentLocation);
return (
<div className="App">
<ul>
{stores.map(store => (
<li key={store.id}>{store.name}</li>
))}
</ul>
</div>
);
}
Working sandbox (without the comments) https://codesandbox.io/embed/eager-elion-0ki0v