I have a custom hook that reads and parses JSON object from the DOM like so:
export const useConfig = () => {
const [config, setConfig] = useState<Config>();
useEffect(() => {
if (document) {
const config = document.getElementById('some-dom-element');
setConfig(JSON.parse(config?.innerHTML || '{}'));
}
}, []);
return config;
};
This object has some meaningful information for different concerns in my application.
So I created another hook that just gets the objects array from the config object:
export const useOptions = () => {
const [options, setOptions] = useState<Options>();
const config = useConfig();
useLayoutEffect(() => {
setOptions(config?.options);
}, [options]);
return options;
};
This is ok, but if I only want to read from the DOM once then I would need to call my useOptions hook at the top level of my component tree and pass the result down to so that components further down the tree can access it.
Is there way that I can change my useOptions hook so that I can call it only in the function components that need it, and not have the DOM read by useConfig on every call?
EDIT: I could probably use Recoil as a lightweight state management solution, but I was wondering if there's a better way to write these hooks so that I don't need to add additional deps
You are describing a custom hook that acts as a singleton.
Try using reusable library, the implementation is simple so you can use it as a recipe:
const useCounter = createStore(() => {
const [counter, setCounter] = useState(0);
return {
counter,
increment: () => setCounter(prev => prev + 1)
}
});
const Comp1 = () => {
const something = useCounter();
}
const Comp2 = () => {
const something = useCounter(); // same something
}
const App = () => (
<ReusableProvider> {/* no initialization code, stores automatically plug into the top provider */}
...
</ReusableProvider>
)
So for your use case, useConfig probably should be in the global store:
const useConfig = {...};
export default createStore(useConfig);
// Always the same config
const config = useConfig();
Related
I created a hook to manipulate users data and one function is listener for users collection.
In hook I created subscriber function and inside that hook I unsubscribed from it using useEffect.
My question is is this good thing or maybe unsubscriber should be inside screen component?
Does my approach has cons?
export function useUser {
let subscriber = () => {};
React.useEffect(() => {
return () => {
subscriber();
};
}, []);
const listenUsersCollection = () => {
subscriber = firestore().collection('users').onSnapshot(res => {...})
}
}
In screen component I have:
...
const {listenUsersCollection} = useUser();
React.useEffect(() => {
listenUsersCollection();
}, []);
What if I, by mistake, call the listenUsersCollection twice or more? Rare scenario, but your subscriber will be lost and not unsubscribed.
But generally speaking - there is no need to run this useEffect with listenUsersCollection outside of the hook. You should move it away from the screen component. Component will be cleaner and less chances to get an error. Also, easier to reuse the hook.
I prefer exporting the actual loaded user data from hooks like that, without anything else.
Example, using firebase 9 modular SDK:
import { useEffect, useMemo, useState } from "react";
import { onSnapshot, collection, query } from "firebase/firestore";
import { db } from "../firebase";
const col = collection(db, "users");
export function useUsersData(queryParams) {
const [usersData, setUsersData] = useState(undefined);
const _q = useMemo(() => {
return query(col, ...(queryParams || []));
}, [queryParams])
useEffect(() => {
const unsubscribe = onSnapshot(_q, (snapshot) => {
// Or use another mapping function, classes, anything.
const users = snapshot.docs.map(x => ({
id: x.id,
...x.data()
}))
setUsersData(users);
});
return () => unsubscribe();
}, [_q]);
return usersData;
}
Usage:
// No params passed, load all the collection
const allUsers = useUsersData();
// If you want to pass a parameter that is not
// a primitive or a string
// memoize it!!!
const usersFilter = useMemo(() => {
return [
where("firstName", "==", "test"),
limit(3)
];
}, []);
const usersFiltered = useUsersData(usersFilter);
As you can see, all the loading and cleaning-up logic is inside the hook, and the component that uses this hook is as clear as possible.
I have a useQuery() hook:
const useQuery = () => {
const router = useRouter();
const build = (query) => {}
const read = () => {}
}
export default useQuery;
I want to be able to use hook in my code like:
const query = useQuery();
useEffect(()=>{
const oldQuery = query.read();
if(oldQuery.length === 0) query.build(newQuery);
},[])
but every time I try to call query.read() I get error Property 'read' does not exist on type 'void'. It looks like scope issue. How can I fix it?
You need a return statement in useQuery:
const useQuery = () => {
const router = useRouter();
const build = (query) => {}
const read = () => {}
return { router, build, read }
}
You may also want to consider memoizing these functions, so that code which consumes this hook doesn't do unnecessary work because it thinks the functions have changed:
const build = useCallback((query) => {}, []);
const read = useCallback((() => {}, []);
I have created a custom hook that fetches setting from an api that uses Async-Storage.
// Takes the key/name of the setting to retrieve
export const useSettings = (key) => {
// stores the json string result in the setting variable
const [setting, setSetting] = useState("");
const deviceStorage = useContext(DeviceStorageContext);
useEffect(() => {
getValue()
.then(value => setSetting(value));
}, []);
// gets value from my custom api that uses Async-Storage to handle r/w of the data.
const getValue = async () => await deviceStorage.getValueStored(key);
const setValue = async (value) => {
await deviceStorage.setValueStored(key, value);
getValue().then(value => setSetting(value));
};
const removeValue = async () => { }
return [setting, { setValue,removeValue }];
};
This works as expected in Main.jsx without any problem.
const Main = () => {
const [units, operations] = useSettings('units');
useEffect(() => {
const initSettings = async () => {
if (units) {
console.log(units)
return;
}
await operations.setValue({ pcs: 1, box: 20 });
};
initSettings();
}, []);
However, when I even just call the useSetting hook in Form.jsx and visit the page, it freezes my entire app to just that page.
const FormView = ({ handleReset, handleSubmit }) => {
const [setting,] = useSettings('units');
Removing the useState and useEffect fixes it and calling these methods directly works but I really don't want to call getValue() throughout my project and use async/await code to handle it.
Stuck on this for hours now. Any help will be appreciated.
Thank you.
It was a dropdown component library inside FormView that was messing it up. Removing that library fixed it.
I am currently asking myself the following question:
Is it recommended that I define my state and logic directly in the ContextProvider or is it okay if I define the state and logic in a separate function to separate the code a bit?
Example:
const MyContext = React.createContext({});
const createStore = () => {
const [myState, setMyState] = useState();
return {
myState,
setMyState
}
}
const MyContextProvider = ({ children }) => {
const store = createStore();
return (
<MyContext.Provider value={store}>{children}</MyContext.Provider>
)
}
I am a little bit affraid of that createStore function. Does the createStore always gets recreated if the Provider rerenders ?
Edit:
Thanks for the answer!
What if I want to use a parameter in the useCreateStore hook ?
Will the parameter gets updated?
Example:
const MyContext = React.createContext({});
const useCustomStore= (myAwesomeValue) => {
const [myState, setMyState] = useState();
const doSomething = useCallback(() => {
//
}, [myAwesomeValue])
return {
myState,
setMyState
}
}
const MyContextProvider = ({ children, title }) => {
const { myState } = useCustomStore(title); //You need to desctructure the returned object here, note myState
return (
<MyContext.Provider value={myState}>{children}</MyContext.Provider>
)
}
What you are trying to create for your "store" is called a custom hook
You will need to make some changes though. It is customary to use 'use' as the start of a custom hook. so, here I have renamed createStore to useCustomStore. Since it is a custom hook with useState, it follows the same rules as if you actually had it within your context provider
Also, your custom hook returns an object which contains the state and a mutation method. you will need to access the state either directly store.myState or you can destructure it { myState} as I have in the example.
const MyContext = React.createContext({});
const useCustomStore= () => {
const [myState, setMyState] = useState();
return {
myState,
setMyState
}
}
const MyContextProvider = ({ children }) => {
const { myState } = useCustomStore(); //You need to desctructure the returned object here, note myState
return (
<MyContext.Provider value={myState}>{children}</MyContext.Provider>
)
}
Is the same as
const MyContext = React.createContext({});
const MyContextProvider = ({ children }) => {
const [myState, setMyState] = useState();
return (
<MyContext.Provider value={myState}>{children}</MyContext.Provider>
)
}
So rerenders will preserve state, since it uses the useState hook.
I don't understand which magic operate here, I try everything is come up in my mind, I can't fix that problem.
I want to use an array which is in the react state of my component in a websocket listener, when the listener is triggered my state is an empty array, however I set a value in an useEffect.
Here my code :
function MyComponent() {
const [myData, setMyData] = useState([]);
const [sortedData, setSortedData] = useState([]);
useEffect(() => {
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
const socket = getSocket(info.socketNamespace); // wrap in socket namespace
handleEvents(socket);
});
}, []);
useEffect(() => {
setSortedData(sortTheArray(myData));
}, [myData]);
const handleEvents = (socket) => {
socket.on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData]; //<=== my probleme is here, whatever I've tried myData is always an empty array, I don't understand why
/**
* onHandleEvent is an external function, just push one new object in the array no problem in the function
*/
onHandleEvent(myDataCloned, payload);
setMyData(myDataCloned);
};
return (
<div>
// display sortedData no problem here
</div>
);
}
Probably missed something obvious, if someone see what.
Thanks !
From the docs:
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
Here handleEvents is called from useEffect on mount and hence it sees only the initial data ([]). To catch this error better, we can move the functions inside useEffect (unless absolutely necessary outside)
useEffect(() => {
const handleEvents = (socket) => {
socket.on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
};
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
const socket = getSocket(info.socketNamespace); // wrap in socket namespace
handleEvents(socket);
});
return () => {
socket.off('EVENT_NAME', handleThisEvent);
}
}, [myData, onHandleEvent]);
Now, you can see that the useEffect has dependencies on myData and onHandleEvent. We did not introduce this dependency now, it already had these, we are just seeing them more clearly now.
Also note that we are removing the listener on change of useEffect. If onHandleEvent changes on every render, you would to wrap that with useCallback in parent component.
Is it safe to omit a function from dependencies - Docs
you need to use useMemo hook to update the function once the array value changed unless Hooks Component will always use initial state value.
change the problem part to this and try
const handleThisEvent = useMemo(payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
setMyData(myDataCloned);
},[the values you need to look for(In this case myData)]);
I finaly come up with a code like that.
useEffect(() => {
let socket;
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
socket = getSocket(info.socketNamespace); // wrap in socket namespace
setSocket(socket)
});
return () => {
if(socket) {
socket.disconnect();
}
}
}, []);
useEffect(() => {
if(socket) {
const handleEvents = (socket) => {
socket.off('EVENT_NAME').on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
};
}
}, [socket, myData, onHandleEvent]);
Thanks to Agney !