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.
Related
I am working on a react app and I have a notification message that appears when the server returns an error or a successful operation.
For example when the input in the login section are wrong, the notification is fired each time I press on login which is annoying and I would like to be fired once and wait a few seconds before it can be fired again.
This is the code from App.js:
import { useAddNotification } from './components/Notifications/NotificationProvider';
const dispatchAddNotification = useAddNotification();
const handleLogin = () => {
axios.post('http://localhost:5000/auth/login', {
username,
password
})
.then(response => {
// code here ...
dispatchAddNotification({ result: "SUCCESS", message: "Succesfully Logged in!" });
})
.catch(error => {
dispatchAddNotification({ result: "ERROR", message: error.msg });
});
}
useAddNotification comes from NotificationProvider.jsx which is the context that wraps the entire app.
const NotificationContext = createContext()
export const useAddNotification = () => {
const dispatch = useContext(NotificationContext);
return (props) => {
dispatch({
type: "ADD_NOTIFICATION",
payload: {
id: v4(),
...props
}
})
}
}
const NotificationProvider = (props) => {
const notifications = []
// second parameter of the callback anonymous function is the initial state
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "ADD_NOTIFICATION":
return [...state, { ...action.payload }];
case "REMOVE_NOTIFICATION":
return state.filter(item => item.id !== action.payload.id);
default:
return state;
}
}, notifications);
return ( ...
How can I make is so that dispatchAddNotification is fired max once every 5 seconds? I tried like this but nope:
export const useAddNotification = () => {
const [isCallable, setIsCallable] = useState(true);
const dispatch = useContext(NotificationContext);
const func = (props) => {
dispatch({
type: "ADD_NOTIFICATION",
payload: {
id: v4(),
...props
}
})
setIsCallable(false)
}
setTimeout(() => {
setIsCallable(true)
}, 10000);
return isCallable && func;
}
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;
}
}
complete reproduce demo: https://github.com/leftstick/hooks-closure-issue
custom hooks as below:
import { useState, useMemo, useCallback } from "react";
import { createModel } from "hox";
function testHooks() {
const [users, setUsers] = useState([
{
id: "a",
name: "hello"
}
]);
const removeUser = useCallback(
user => {
return new Promise(resolve => {
setTimeout(() => {
// async work here
setUsers(users.filter(u => u.name !== user.name));
resolve();
}, 10);
});
},
[users]
);
const addUser = useCallback(
user => {
return new Promise(resolve => {
setTimeout(() => {
// async work here
// users here is not up-to-date
setUsers([...users, user]);
resolve();
}, 10);
});
},
[users]
);
//modify user = remove old-user + add new-user
const modifyUser = useCallback(
(oldUser, newUser) => {
return new Promise((resolve, reject) => {
removeUser(oldUser)
.then(() => {
// addUser relies on latest users updated by removeUser
// but due to closure issue, it refers to original value
// what is the best approach to achieve the expected behivour?
return addUser(newUser);
})
.then(resolve, reject);
});
},
[users]
);
return {
users,
modifyUser
};
}
export default createModel(testHooks);
As well as with this.setState for class-based components functional version of setter saves the day
const removeUser =
user => {
return new Promise(resolve => {
setTimeout(() => {
setUsers(users => users.filter(u => u.name !== user.name));
resolve();
}, 10);
});
}
;
Meanwhile chained updates looks suspicious. Having long enough chain if some step fails you may get into trouble trying to revert component to some consistent state. In general case I'd accumulate all data needed and then update component at once.
My app uses React, Redux and Thunk.
Before my app renders I wish to dispatch some data to the store.
How can I make sure the ReactDOM.render() is run after all dispatches has finished?
See my code below
index.js
const setInitialStore = () => {
return dispatch => Promise.all([
dispatch(startSubscribeUser()),
dispatch(startSubscribeNotes()),
]).then(() => {
console.log('initialLoad DONE')
return Promise.resolve(true)
})
}
store.dispatch(setInitialStore()).then(()=>{
console.log('Render App')
ReactDOM.render(jsx, document.getElementById('app'))
})
Actions
export const setUser = (user) => ({
type: SET_USER,
user
})
export const startSubscribeUser = () => {
return (dispatch, getState) => {
const uid = getState().auth.id
database.ref(`users/${uid}`)
.on('value', (snapshot) => {
const data = snapshot.val()
const user = {
...data
}
console.log('user.on()')
dispatch(setUser(user))
})
}
}
export const setNote = (note) => ({
type: SET_NOTE,
note
})
export const startSubscribeNotes = () => {
return (dispatch, getState) => {
database.ref('notes')
.on('value', (snapshot) => {
const data = snapshot.val()
const note = {
...data
}
console.log('note.on()')
dispatch(setNote(note))
})
}
}
My log shows
"initialLoad DONE"
"Render App"
...
"user.on()"
"note.on()"
What I expect is for user.on() and note.on() to be logged before initialLoad DONE and Render App
Many thanks! /K
I'm pretty sure this is because startSubscribeUser and startSubscribeNotes don't return a function returning a promise.
Then, what happens in this case, is that the database.ref is not waited to be completed before executing what's in the next then.
I don't know exactly what that database variable is, but this should work :
return new Promise(resolve => {
database.ref(`users/${uid}`)
.on('value', (snapshot) => {
const data = snapshot.val()
const user = {
...data
}
console.log('user.on()')
dispatch(setUser(user))
resolve()
})
})
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
}