GET requests canceling fine in this example:
export default function Post (props) {
const _cancelToken = axios.CancelToken.source()
useEffect(() => {
const _loadAsyncData = async () => {
await axios.get('/post'), { cancelToken: _cancelToken.token })
}
_loadAsyncData()
return () => {
_cancelToken.cancel()
}
}, [])
return ()
}
But when I need save form via POST request, my code looks like:
export default function Form (props) {
const _cancelToken = axios.CancelToken.source()
const _zz = { qq: 'QQ' }
const handleCreate = async e => {
e.preventDefault()
_zz.qq = 'ZZ'
await axios.post('/form'), {}, { cancelToken: _cancelToken.token })
}
useEffect(() => {
return () => {
console.log(_zz.qq)
_cancelToken.cancel()
}
}, [])
return ()
}
Request not cancel and my _zz.qq always 'QQ' instead 'ZZ'. It's working fine without hooks, but I like hooks and want to use hooks for new components.
I want to cancel request when componentWillUnmount.
This is because you're losing the changes between renders. During the handleCreate call the variable changes only for that render. When the useEffect is run on a subsequent render/unmounting, you're resetting _zz to { qq: 'QQ' }. In order to get around this you need to use references.
export default function Form (props) {
const cancelToken = useRef(null)
const zz = useRef({ qq: 'QQ' })
const handleCreate = async e => {
e.preventDefault()
cancelToken.current = axios.CancelToken.source()
zz.current = { qq: 'ZZ' }
await axios.post('/form'), {}, { cancelToken: cancelToken.current.token })
}
useEffect(() => {
return () => {
console.log(zz.current) //this should now be {qq : 'ZZ'}
if (cancelToken.current) {
cancelToken.current.cancel()
}
}
}, [])
return null
}
Related
How should I prevent a re render when I click on onClose to close the modal.
It looks like the dispatch function: dispatch(setActiveStep(0) cause some troubles.
export default function ImportOrderModal(props: ImportOrderModalProps) {
const { open, onClose } = props
const {
orderImport: { activeStep }
} = useAppSelector((state: RootState) => state)
const steps = useImportOrderConfig()
const dispatch = useDispatch()
React.useEffect(() => {
getOrdersList()
}, [])
const onCloseModal = () => {
onClose()
// Force a re render because of activeStep value
dispatch(setActiveStep(0))
}
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
return (
<Modal open={open} onClose={onCloseModal}>
<Stepper steps={steps} currentStepNumber={activeStep} />
<FormSteps />
</Modal>
)
}
This block is outside of your useEffect()
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
This will cause rendering troubles.
if you're using an older version of React (<17) that doesn't enforce <React.StrictMode> you can get away with rewriting that as:
useEffect(() => {
getOrderList
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
}, [dispatch]);
if you're using a newer version of React (>18) you will have to cleanup your asynchronous call in the cleanup function of your useEffect().
useEffect(() => {
// This has to be passed down to your fetch/axios call
const controller = new AbortController();
getOrderList(controller)
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
return () => {
// This will abort any ongoing async call
controller.abort();
}
}, [dispatch]);
For this to make sense I will probably have to write an example of the api call for you as well, if you don't mind I'll use axios for the example but it essentially works the same-ish with .fetch.
const getOrderList = async (controller) => {
try {
const { data } = await axios.get("url", { signal: controller.signal });
return data.orders;
} catch (e) {
throw e;
}
}
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();
export async function onGetNews(){
let data = await axios.get(`${Link}/news`, {
params: {
limit: 1
}
}).then(res => {
return (res.data)
});
return data
}
I tried a lot of solutions and I didn't find a good one. I use limit and other ... and when I use useEffect with export function it gives me an error
export function OnGetServices(){
const [service, setService] = useState([])
useEffect(() => {
setTimeout(async () => {
let data = await axios.get(`${Link}/services`, {}).then(res => {
setService(res.data)
});
}, 1000);
console.log(data);
}, []);
console.log(service);
return service;
}
Why are you doing .then() when you are using async/await? Try this:
export async function onGetNews(){
let res= await axios.get(`${Link}/news`, {
params: {
limit: 1
}
});
return res.data
}
And your react snippet can be:
export function OnGetServices(){
const [service, setService] = useState([])
useEffect(() => {
setTimeout(async () => {
let res = await axios.get(`${Link}/services`, {})
setService(res.data);
console.log(res.data);
}, 1000);
}, []);
}
And if you don't really need the setTimeout, you could change the implementation to:
export function OnGetServices(){
const [service, setService] = useState([])
useEffect(() => {
const fn = async () => {
let res = await axios.get(`${Link}/services`, {})
setService(res.data);
console.log(res.data);
}
fn();
}, []);
}
Async/await drives me crazy either. I wrote a solution, but I'm not sure if it performs good practices. Feedback appreciated.
https://codesandbox.io/s/react-boilerplate-forked-y89eb?file=/src/index.js
If it's a hook then it has to start with the "use" word. Only in a hook, or in a Component, you can use hooks such as useEffect, useState, useMemo.
export function useService(){ //notice the "use" word here
const [service, setService] = useState([])
useEffect(() => {
setTimeout(async () => {
let data = await axios.get(`${Link}/services`, {}).then(res => {
setService(res.data)
});
}, 1000);
console.log(data);
}, []);
console.log(service);
return service;
}
const SomeComponent = () => {
const service = useService();
}
I have a component which is updating states on page load with useeffect. But when i use history.goback() function to go back from a component to my bank component useeffect is not working. Here my code ;
bottomnavigator.js :
const goBack = () => {
if (props.history.location.pathname !== "/app/phone" && props.history.location.pathname !== "/") {
props.history.goBack();
}
};
bank.js :
const [transactions, settransactions] = useState([]);
const [bankBalance, setbankBalance] = useState(0);
const getBankData = async () => {
if (props.location.state) {
const res = await Nui.post(props.location.state, {});
if (res) {
setbankBalance(res);
}
}
}
const getTransactionData = async () => {
if (props.location.state) {
const res = await Nui.post('GET_BANK_TRANSACTIONS', {});
if (res) {
settransactions(res);
}
}
}
useEffect(() => {
getBankData();
getTransactionData();
}, []);
The issue is that you have listed "no dependencies" ([]) in your useEffect, so it will run once at the beginning and then never again. You should either omit the dependencies like so:
useEffect(() => {
getBankData();
getTransactionData();
});
Or list the correct set of dependencies like so: (I'm guessing here based on your code, but hopefully I got it right)
useEffect(() => {
getBankData();
getTransactionData();
}, [props.location.state]);
I have an application required to run API calls every 3 seconds. I used useInterval to call API, every 3 seconds I received the API result. When I update from redux, something went wrong with useInterval.
UseInterval
export default function useInterval(callback, delay, immediate = true) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
(async() => {
async function tick() {
await savedCallback.current();
}
if (delay !== null) {
if (immediate) {
await tick();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
})();
}, [delay]);
}
Main
const enhance = connect(
(state, ownProps) => ({
modal: state.modal[ownProps.id]
}),
{ updateFromRedux }
);
const container = ({ id, modal, updateFromRedux }) => {
useInterval(() => {
# -----> This scope of codes went wrong when updateFromRedux is called <-----
let modalId = modal["id"]
return fetch(`https://api-url.com/${modalId}`)
.then(res => res.json())
.then(
(result) => {
updateFromRedux(id, result)
}
)
}, 3000)
})
export default enhance(container);
Redux
export const updateFromRedux = (id, details) => ({
type: UPDATE_DETAILS,
payload: { id, details }
});
Problem
The modalId produces an inconsistent output such as undefined inside useInterval after updateFromRedux redux method is called.