How can I initialize in useState with the data from custom hooks? - reactjs

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

Related

How do I initialise state values and methods that uses useSyncExternalStore + Context in React?

Description
I'm creating a state management tool for a small project, using mainly useSyncExternalStore from React, inspired by this video from Jack Herrington https://www.youtube.com/watch?v=ZKlXqrcBx88&ab_channel=JackHerrington.
But, I'm running into a pattern that doesn't look right, which is having to use 2 providers, one to create the state, and the other to initialise it.
The gist of the problem:
I have a property sessionId coming from an HTTP request. Saving it in my store wasn't an issue.
However, once I have a sessionId then all of my POST requests done with notifyBackend should have this sessionId in the request body. And I was able to achieve this requirement using the pattern above, but I don't like it.
Any idea how to make it better ?
Code
CreateStore.jsx (Not important, just providing the code in case)
export default function createStore(initialState) {
function useStoreData(): {
const store = useRef(initialState);
const subscribers = useRef(new Set());
return {
get: useCallback(() => store.current, []),
set: useCallback((value) => {
store.current = { ...store.current, ...value };
subscribers.current.forEach((callback) => callback());
}, []),
subscribe: useCallback((callback) => {
subscribers.current.add(callback);
return () => subscribers.current.delete(callback);
}, []),
};
}
const StoreContext = createContext(null);
function StoreProvider({ children }) {
return (
<StoreContext.Provider value={useStoreData()}>
{children}
</StoreContext.Provider>
);
}
function useStore(selector) {
const store = useContext(StoreContext);
const state = useSyncExternalStore(
store.subscribe,
() => selector(store.get()),
() => selector(initialState),
);
// [value, appendToStore]
return [state, store.set];
}
return {
StoreProvider,
useStore,
};
}
Creating the state
export const { StoreProvider, useStore } = createStore({
sessionId: "INITIAL",
notifyBackend: () => { },
});
index.jsx
<Router>
<StoreProvider>
<InitialisationProvider>
<App />
</InitialisationProvider>
</StoreProvider>
</Router
InitialisationContext.jsx
const InitialisationContext = createContext({});
export const InitializationProvider = ({ children }) {
const [sessionId, appendToStore] = useStore(store => store.session);
const notifyBackend = async({ data }) => {
const _data = {
...data,
sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
appendToStore({ sessionId: result.sessionId });
} else if (result.otherProp) {
appendToStore({ otherProp: result.otherProp });
}
} catch (e) { }
};
useEffect(() => {
appendToStore({ notifyBackend });
}, [sessionId]);
return (
<InitialisationContext.Provider value={{}}>
{children}
</InitialisationContext.Provider>
);
}
I just tried out Zustand, and it's very similar to what I'm trying to achieve.
Feels like I'm trying to reinvent the wheel.
With Zustand:
main-store.js
import create from 'zustand';
export const useMainStore = create((set, get) => ({
sessionId: 'INITIAL',
otherProp: '',
notifyBackend: async ({ data }) => {
const _data = {
...data,
sessionId: get().sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
set({ sessionId: result.sessionId });
} else if (result.otherProp) {
set({ otherProp: result.otherProp });
}
} catch (e) { }
},
}));
SomeComponent.jsx
export const SomeComponent() {
const sessionId = useMainStore(state => state.sessionId);
const notifyBackend = useMainStore(state => state.notifyBackend);
useEffect(() => {
if (sessionId === 'INITIAL') {
notifyBackend();
}
}, [sessionId]);
return <h1>Foo</h1>
};
This answer focuses on OPs approach to createStore(). After reading the question a few more times, I think there are bigger issues. I'll try to get to these and then extend the answer.
Your approach is too complicated.
First, the store is no hook! It lives completely outside of react. useSyncExternalStore and the two methods subscribe and getSnapshot are what integrates the store into react.
And as the store lives outside of react, you don't need a Context at all.
Just do const whatever = useSyncExternalStore(myStore.subscribe, myStore.getSnapshot);
Here my version of minimal createStore() basically a global/shared useState()
export function createStore(initialValue) {
// subscription
const listeners = new Set();
const subscribe = (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
}
const dispatch = () => {
for (const callback of listeners) callback();
}
// value management
let value = typeof initialValue === "function" ?
initialValue() :
initialValue;
// this is what useStore() will return.
const getSnapshot = () => [value, setState];
// the same logic as in `setState(newValue)` or `setState(prev => newValue)`
const setState = (arg) => {
let prev = value;
value = typeof arg === "function" ? arg(prev) : arg;
if (value !== prev) dispatch(); // only notify listener on actual change.
}
// returning just a custom hook
return () => useSyncExternalStore(subscribe, getSnapshot);
}
And the usage
export const useMyCustomStore = createStore({});
// ...
const [value, setValue] = useMyCustomStore();

Warning: Cannot update a component from inside the function body of a different component in React Native

i have loading screen for call all the data function.i used async function for all function call.
//NOTE: this screen loads all the data and store it in store so user will have a smother experience
const LoadingScreen = (props) => {
const gotToHomeScreen = () => {
props.navigation.replace("Home", { screen: HOME_SCREEN });
};
//NOTE: loading data here for home screen journey
const getRequiredAPIDataInStore = async () => {
GetAllFieldProp();
GetAllSalaryAPIResponse();
GetSalaryAPIResponse();
let { spinnerStateForm101 } = GetForm101API();
let { spinnerStateForm106 } = GetForm106API();
GetMessagesCountAPI();
GetMessagesAPI(props);
GetAllFormAPIResponse();
GetAllSpecificSalaryAPIResponse();
let { spinnerStateMonthly } = GetMonthlyAbsenceAPI(props);
let { spinnerStateWeekly } = GetWeeklyAbsenceAPI(props);
if (
spinnerStateMonthly &&
spinnerStateWeekly &&
spinnerStateForm106 &&
spinnerStateForm101
) {
gotToHomeScreen();
}
};
getRequiredAPIDataInStore();
export default LoadingScreen;
but i am getting warning messages for this.
Warning: Cannot update a component from inside the function body of a different component.
at src/screens/loading-screen.js:19:26 in gotToHomeScreen
at src/screens/loading-screen.js:37:6 in getRequiredAPIDataInStore
How to solve this warning messsage?
Here's the approach I would take.
const Loading = () => {
const [spinnerStateMonthly, setSpinnerStatMonthly] = useState(null);
const [spinnerStateWeekly, setspinnerStateWeekly] = useState(null);
const [spinnerStateForm106, setspinnerStateForm106] = useState(null);
const [spinnerStateForm101, setSpinnerStateForm101] = useState(null);
const gotToHomeScreen = () => {
props.navigation.replace("Home", { screen: HOME_SCREEN });
};
useEffect(() => {
// async callback to get all the data and set state
(async () => {
await GetAllFieldProp();
await GetAllSalaryAPIResponse();
await GetSalaryAPIResponse();
const { spinnerStateForm101: local101 } = GetForm101API();
const { spinnerStateForm106: local106 } = GetForm106API();
setSpinnerStateForm101(local101);
setSpinnerStateForm106(local106);
await GetMessagesCountAPI();
await GetMessagesAPI(props);
await GetAllFormAPIResponse();
await GetAllSpecificSalaryAPIResponse();
const { spinnerStateMonthly: localMonthly } = GetMonthlyAbsenceAPI(props);
const { spinnerStateWeekly: localWeekly } = GetWeeklyAbsenceAPI(props);
setSpinnerStateMonthly(localMonthly);
setSpinnerStateWeekly(localWeekly);
})();
}, []);
// effect to check for what the state is and if all the states are satisfied
// then go to the home screen
useEffect(() => {
if (spinnerStateMonthly
&& spinnerStateWeekly
&& spinnerStateForm106
&& spinnerStateForm101) {
gotToHomeScreen();
}
}, [spinnerStateMonthly, spinnerStateWeekly, spinnerStateForm101,
spinnerStateForm106]);
};

react/ state not being updated because of cleanup

I have custom hook that adds user info for posts created. And if I don't add cleanup it works as intended. I create new post, press post and it gets added to screen, but with if(mounted.current)setState() it does not update, only on refresh. What could be the problem and how could I fix it?
const AllPostsAssign = () => {
const { userPosts, allUsers } = useData();
const [posts, setPosts] = useState();
const mounted = useRef(true);
// map existing posts and add user object and post id into post object.
useEffect(() => {
const postsWithUsers = allUsers.map((y) => {
const usersAssignedToPosts = userPosts.map((x) => {
if (y.userId === x.data().userId) {
const q = Object.assign(x.data(), { id: x.id });
const z = Object.assign(q, { user: y });
return z;
}
});
return usersAssignedToPosts;
});
const noUndefined = postsWithUsers.flat().filter((post) => post);
// without mounted.current it works.
if (noUndefined && mounted.current) setPosts(noUndefined);
console.log(mounted.current);
console.log(posts);
return () => (mounted.current = false);
}, [userPosts, allUsers]);
// sort by time created and then reverse, so newest posts would be on top.
posts &&
posts.sort((a, b) => {
return a.createdAt.seconds - b.createdAt.seconds;
});
posts && posts.reverse();
return posts;
};
export default AllPostsAssign;
Have your mounted check declared directly inside your useEffect, as such:
useEffect(() => {
let mounted = true;
const postsWithUsers = allUsers.map((y) => {
const usersAssignedToPosts = userPosts.map((x) => {
if (y.userId === x.data().userId) {
const q = Object.assign(x.data(), { id: x.id });
const z = Object.assign(q, { user: y });
return z;
}
});
return usersAssignedToPosts;
});
const noUndefined = postsWithUsers.flat().filter((post) => post);
// without mounted.current it works.
if (noUndefined && mounted) setPosts(noUndefined);
console.log(mounted);
console.log(posts);
return () => (mounted = false);
}, [userPosts, allUsers]);

react state not updating inside callabck

I'm not understanding why the following code, the callback onSocketMessage is not using the new acquisition state. inside the useEffect the state is correctly updated, but the function is not evaluated again...i've also tryed using useCallback with acquisition as dependency but nothing changed.
const Ac = () => {
const [acquisition, setAcquisition] = useState({ data: {} })
const [loading, setLoading] = useState(true)
const socket = useRef(null);
const onSocketMessage = (message) => {
console.log(acquisition) // this is always initial state
let { data } = acquisition
data.input[message.index] = message.input
setAcquisition(prevState => ({ ...prevState, data }));
}
useEffect(() => {
fetchCurrentAcquisition(acquisition => {
setAcquisition(acquisition)
setLoading(false)
socket.current = newSocket('/acquisition', () => console.log('connected'), onSocketMessage);
})
return () => socket.current.disconnect()
}, [])
console.log(acquisition)
You are logging a stale closure you should try the following instead:
const onSocketMessage = useCallback((message) => {
setAcquisition((acquisition) => {
//use acquisition in the callback
console.log(acquisition);
//you were mutating state here before
return {
...acquisition,
data: {
...acquisition.data,
input: {
//not sure if this is an array or not
//assimung it is an object
...acquisition.data.input,
[message.index]: message.input,
},
},
};
});
}, []); //only created on mount
useEffect(() => {
fetchCurrentAcquisition((acquisition) => {
setAcquisition(acquisition);
setLoading(false);
socket.current = newSocket(
'/acquisition',
() => console.log('connected'),
onSocketMessage
);
});
return () => socket.current.disconnect();
//onSocketMessage is a dependency of the effect
}, [onSocketMessage]);

React, What happens if multiple effects setState a single value?

Is it safe to do something like the following?
const [foo, setFoo] = useState(undefined)
useEffect(() => {
dispatch(someFunc()).then(response => {
let { someFoo } = response
setFoo(someFoo)
})
}, []) // or }, [bar])
useEffect(() => {
dispatch(anotherFunc()).then(response => {
let { anotherFoo } = response
setFoo(anotherFoo)
})
}, [bar])
The effects are executed in the given order and only the "foo" from the last effect setter will be visible in the UI. For instance, the following component will output bar - 1:
const Component = ({ bar }) => {
const [foo, setFoo] = useState(undefined);
console.log("render", bar, foo);
useEffect(() => {
console.log("effect 1");
let someFoo = bar + 1;
setFoo(someFoo);
}, [bar]);
useEffect(() => {
console.log("effect 2");
let anotherFoo = bar - 1;
setFoo(anotherFoo);
}, [bar]);
return (
<div>
{bar} sets {foo}
</div>
);
};
https://codesandbox.io/s/proud-leaf-g55n9?file=/src/App.js:76-482
EDIT: If you use [] as the dependency array, it will only execute once. If there's an async function inside the effect like fetch, the last executed setFoo will prevail. The following example will display random results in each click:
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(1);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(2);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
Example 2:
https://codesandbox.io/s/jovial-architecture-0ybcz?file=/src/App.js

Resources