I have a variable "second", which needs to be updated every second. and it was used by multiple components so i placed it in context.
Only for the first time i see correct value and from the next iteration it is coming as undefined.
any advice please
Output:
state is {second: 43, error: null}
state is {second: undefined, error: null}
and i'm using setInterval inside useEffect to update the variable every second
Code
import React, { useReducer, useEffect } from "react";
export const SecContext = React.createContext();
const initialState = {
second: new Date().getSeconds(),
error: null,
};
const reducer = (state, action) => {
switch (action.type) {
case "UPDATE_SECOND":
return {
...state,
second: action.second,
};
default:
throw new Error();
}
};
export const SecContextProvider = (props) => {
const [state, dispatch] = useReducer(reducer, initialState);
const updateSecond = () => {
let second = new Date().getSeconds();
console.log("state is ", state);
dispatch({
type: "UPDATE_SECOND",
payload: second,
});
};
useEffect(() => {
const timeoutId = setInterval(() => {
updateSecond();
}, 1000);
return function cleanup() {
clearInterval(timeoutId);
};
}, [updateSecond]);
return (
<SecContext.Provider value={[state, dispatch]}>
{props.children}
</SecContext.Provider>
);
};
export default SecContextProvider;
Sorry for writing this answer, but I am not able to add a new comment.
A minor issue I saw
return {
...state,
second: action.second, // you refer to action.second, which is undefined, you need action.payload here
};
Related
I apologize for the ignorance on my part but I can't understand why this code works until the page is refreshed. The main goal is to utilize the array.reduce method on the response.json from my fetch so that I can have a rolling sum of all of the json[].amount element displayed in my component. This code works until the page is refreshed and then it says: Uncaught TypeError: Cannot read properties of null (reading 'reduce') at Balance (Balance.js:27:1). I know at the very least I'm doing something wrong with the state.
Balance.js component:
import { useEffect } from "react";
import {useBalancesContext} from '../hooks/useBalancesContext'
import { useAuthContext } from '../hooks/useAuthContext'
function Balance(){
const {balances, dispatch} = useBalancesContext()
const {user} = useAuthContext()
useEffect(() => {
const addBalances = async () => {
const response = await fetch("http://localhost:4000/api/balances", {
headers: {
'Authorization': `Bearer ${user.token}`
}
});
const json = await response.json();
if (response.ok) {
dispatch({type: 'SET_BALANCES', payload: json})
}
}
if (user) {
addBalances();
}
}, [dispatch, user]);
const newBalance = balances.reduce((accumulator, currentValue) => accumulator + currentValue.amount, 0)
return (
<div className="header">
<strong>Available Balance: ${newBalance}</strong>
</div>
)
}
export default Balance;
BalanceContext.js:
import { createContext, useReducer } from "react";
export const BalancesContext = createContext();
export const balancesReducer = (state, action) => {
switch (action.type) {
case 'SET_BALANCES':
return {
balances: action.payload
}
/* case 'SUM_BALANCES':
return {
balances: state.balances.amount.reduce((accumulator, currentValue) => accumulator + currentValue.amount, 0)
} */
case 'CREATE_BALANCE':
return {
balances: [action.payload, ...state.balances]
}
case 'DELETE_BALANCE':
return {
balances: state.balances.filter((b) => b._id !== action.payload._id)
}
default:
return state
}
}
export const BalancesContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(balancesReducer, {
balances: null
})
return (
<BalancesContext.Provider value={{...state, dispatch}}>
{children}
</BalancesContext.Provider>
)
};
I've tried using the array.reduce method inside of the function but then I don't have access to the newBalance value inside of Balance component. I've also tried using array.reduce in the reducer as well but I'm not confident I was on the right track with that either.
balances is initialially null, so its value may not be set when the page is loaded.
A good approach with values calculated with .reduce() is to use the useMemo hook:
import { useMemo } from 'react';
const newBalance = useMemo(() => {
if (!balances) return 0;
return balances.reduce(
(accumulator, currentValue) => accumulator + currentValue.amount,
0
);
}, [balances]);
This will tell React to calculate newBalance only when balances changes and will return 0 if balances is not already set.
I have a react application with two buttons, which on click load user name from server. The behaviour works if I click buttons one at a time and wait for response, however, if I click both, the response from API for second button writes value to state which is stale due to which the first button gets stuck in loading state. How can I resolve this to always have latest data when promise resolves?
Code sandbox demo: https://codesandbox.io/s/pensive-frost-qkm9xh?file=/src/App.js:0-1532
import "./styles.css";
import LoadingButton from "#mui/lab/LoadingButton";
import { useRef, useState } from "react";
import { Typography } from "#mui/material";
const getUsersApi = (id) => {
const users = { "12": "John", "47": "Paul", "55": "Alice" };
return new Promise((resolve) => {
setTimeout((_) => {
resolve(users[id]);
}, 1000);
});
};
export default function App() {
const [users, setUsers] = useState({});
const availableUserIds = [12, 47];
const loadUser = (userId) => {
// Mark button as loading
const updatedUsers = { ...users };
updatedUsers[userId] = {
id: userId,
name: undefined,
isLoading: true,
isFailed: false
};
setUsers(updatedUsers);
// Call API
getUsersApi(userId).then((userName) => {
// Update state with user name
const updatedUsers = { ...users };
updatedUsers[userId] = {
...updatedUsers[userId],
name: userName,
isLoading: false,
isFailed: false
};
setUsers(updatedUsers);
});
};
return (
<div className="App">
{availableUserIds.map((userId) =>
users[userId]?.name ? (
<Typography variant="h3">{users[userId].name}</Typography>
) : (
<LoadingButton
key={userId}
loading={users[userId]?.isLoading}
variant="outlined"
onClick={() => loadUser(userId)}
>
Load User {userId}
</LoadingButton>
)
)}
</div>
);
}
The problem is that useState's setter is asynchronous, so, in your loader function, when you define const updatedUsers = { ...users };, user is not necessary updated.
Luckily, useState's setter provides allows us to access to the previous state.
If you refactor your code like this, it should work:
const loadUser = (userId) => {
// Mark button as loading
const updatedUsers = { ...users };
updatedUsers[userId] = {
id: userId,
name: undefined,
isLoading: true,
isFailed: false
};
setUsers(updatedUsers);
// Call API
getUsersApi(userId).then((userName) => {
// Update state with user name
setUsers(prevUsers => {
const updatedUsers = { ...prevUsers };
updatedUsers[userId] = {
...updatedUsers[userId],
name: userName,
isLoading: false,
isFailed: false
};
return updatedUsers
});
});
};
Here a React playground with a simplified working version.
I want to fetch data using useReducer and useEffect
getPeople is a function that return the result from https://swapi.dev/documentation#people
when i console log results the data is fetching correctly but the state is not updated.
My functional component:
export default function App () {
const [state, dispatch] = useReducer(reducer, initialSate);
useEffect(async () => {
const { results } = await getPeople();
dispatch({ type: FETCH_PEOPLE, payload: results });
}, []);
return (
<>
<GlobalStyles/>
{state.loading
? <Loader size={'20px'} />
: <>
<Search></Search>
<People />
</>
}
</>
);
}
My reducer function:
export const reducer = (state = initialSate, action) => {
switch (action.type) {
case FETCH_PEOPLE:
return {
...state,
loading: false,
results: action.payload,
counter: state.counter
};
case INCREMENT_COUNTER:
return {
...state,
loading: true,
counter: increment(state.counter)
};
case DECREMENT_COUNTER:
return {
...state,
loading: true,
counter: decrement(state.counter)
};
default:
return state;
}
};
TLDR;
You need to wrap the async action in an IIFE:
useEffect(() => {
(async() => {
const { results } = await getPeople();
dispatch({ type: FETCH_PEOPLE, payload: results });
})();
}, []);
Details:
If you do the OP version in typescript then it will give out an error like:
Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'
The useEffect hook expects a cleanup function to be returned (optionally). But if we go ahead with OP's version, the async function makes the callback function return a Promise instead.
How to execute the axios part and send the updated states props to Important component.
When I console.log I see that state passed as props with an empty object but after a fraction of seconds again states is updated with a new fetched value that means my return is running first then my usEffect axios part is running,
How can I make sure that axios part should run first then my return part. In first go updated part should be sent not the blank empty part
const initialState = {
Important: [{}],
Error: false
}
const reducer = (state, action) => {
switch (action.type) {
case "STEPFIRST":
return {
...state,
Important: action.payload,
};
case "STEPSecond":
return {
Error: true,
};
default:
return state;
}
}
const Landing = () => {
const [states, dispatch] = useReducer(reducer, initialState)
console.log(states)
useEffect(() => {
axios.get("https://example.com/")
.then(response => {
dispatch({
type: "STEPFIRST",
payload: response.data
});
})
.catch(error => {
dispatch({
type: "STEPSecond"
});
});
},[]);
const [xyz, xyzfn] = useState();
console.log(xyz)
return (
<div>
<Important states = {states} xyzfn={xyzfn} />
<Foo xyz={xyz}/>
</div>
);
};
export default Landing;
useEffect will always run after first rendering is done. You can have a loading state in your state and return the component accordingly.
const initialState = {
Important: [{}],
Error: false,
isLoading: true
}
const reducer = (state, action) => {
switch (action.type) {
case "STEPFIRST":
return {
...state,
Important: action.payload,
isLoading: false
};
case "STEPSecond":
return {
Error: true,
isLoading: false
};
default:
return state;
}
}
const Landing = () => {
const [states, dispatch] = useReducer(reducer, initialState)
console.log(states)
useEffect(() => {
axios.get("https://example.com/")
.then(response => {
dispatch({
type: "STEPFIRST",
payload: response.data
});
})
.catch(error => {
dispatch({
type: "STEPSecond"
});
});
},[]);
const [xyz, xyzfn] = useState();
console.log(xyz)
if(state.isLoading){
return <div>Loading....</div>
}
return (
<div>
<Important states = {states} xyzfn={xyzfn} />
<Foo xyz={xyz}/>
</div>
);
};
useEffect callback runs after the render phase.
Also, fetch calls are asynchronous, so you want to use conditional rendering:
const Landing = () => {
const [states, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
axios
.get("https://example.com/")
.then((response) => {
dispatch({
type: "STEPFIRST",
payload: response.data,
});
})
.catch((error) => {
dispatch({
type: "STEPSecond",
});
});
}, []);
// Use any comparison function to indicate that `states` changed.
// like deep comparison function `isEqual` from lodash lib.
return (
<div>
{!lodash.isEqual(states, initialState) && (
<Important states={states} xyzfn={xyzfn} />
)}
</div>
);
};
I have a situation where i can successfully dispatch my states with reducers and i can render it in my component
Here the relevant code
in my action/index.js
export const receivedLeaguesList = json => ({
type: RECEIVE_LEAGUES_LIST,
json: json
});
export function fetchLeaguesList() {
return function(dispatch) {
dispatch(requestLeaguesList());
return axios
.get("https://www.api-football.com/demo/v2/leagues/")
.then(res => {
let leagues = res.data.api.leagues;
dispatch(receivedLeaguesList(leagues));
})
.catch(e => {
console.log(e);
});
}
}
my reducers/index.js
import { REQUEST_LEAGUES_LIST, RECEIVE_LEAGUES_LIST } from "../actions";
const initialState = {
leaguesList: [],
isLeagueListLoading: false
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case REQUEST_LEAGUES_LIST:
return { ...state, isLeagueListLoading: true };
case RECEIVE_LEAGUES_LIST:
return { ...state, leaguesList: action.json, isLeagueListLoading: false };
default:
return state;
}
};
in my component component/Leagues.js
let Leagues = ({ leaguesList, loading, getList }) => {
useEffect(() => {
getList();
}, [getList]);
const [itemsLeagues] = useState([leaguesList]);
console.log("league list", itemsLeagues);
const mapDispatchToProps = {
getList: fetchLeaguesList
};
I have reproduced the demo here => https://codesandbox.io/s/select-demo-71u7h?
I can render my leaguesList states in my component doing the map, but why when
const [itemsLeagues] = useState([leaguesList]);
console.log("league list", itemsLeagues);
returns an empty array ?
See the image
You're setting useState's init value wrong:
const [itemsLeagues] = useState(leaguesList);
instead of
const [itemsLeagues] = useState([leaguesList]);
The return value of useState isn't the value itself, but the array of value and mutator:
const [value, setValue] = useState([42, 43])
// here's value equals [42, 43]
So if you were trying to destructure the wrapping array you passed to useState(), you should use it like this (though you don't need it):
const [[itemsLeagues]] = useState([leaguesList]);