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.
Related
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
};
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]);
I'm learning Redux, and I am very confused about what is going on here. I am using thunk and GET_ITEMS is in my reducer so I'm not sure what I have done wrong? The error is in the dispatch(getItemsAction());
Redux.js
function reducer(state, action) {
switch (action.type) {
case 'GET_ITEMS':
return {
...state,
items: action.payload,
loading: false,
};
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
};
case 'DELETE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
};
case 'ITEMS_LOADING':
return {
...this.state,
loading: true,
};
default:
return state;
}
}
export const getItemsAction = () => ({
return(dispatch) {
axios.get('api/items').then(response => {
console.log(response);
dispatch({ type: 'GET_ITEMS', payload: response.data });
});
},
});
ShoppingList.js
import { addItemAction, deleteItemAction, getItemsAction } from '../redux';
export default function ShoppingList() {
const items = useSelector(state => state.items);
const dispatch = useDispatch();
const addItem = name => dispatch(addItemAction(name));
const deleteItem = id => dispatch(deleteItemAction(id));
useEffect(() => {
dispatch(getItemsAction());
}, []);
in the top code you returned the dispatch in incorrect way
but actually you need to call dispatch like cb
for example in javascript we do somthing like this
const myfunc = () => cb => {
cb('OK')
};
its callback in javascript and you have to return dispatch like callback to work correct
export const getItemsAction = () => dispatch => {
axios.get('api/items').then(response => {
dispatch({
type: 'GET_ITEMS',
payload: response.data
})
});
};
at the end dont forgot to get axios response data with response.data
the correct syntax for the action is
export const getItemsAction = () => dispatch => {
axios.get('/api/items').then(res =>
dispatch({
type: 'GET_ITEMS',
payload: res.data,
})
);
};
I want to implement an action which gets item by id, so I've created fetchItemAction(), as follows:
export const fetchItemAction = () => (dispatch) => {
dispatch({
type: FETCH_ITEM_REQUEST,
});
return axios.get(`${url}/notes/5d4724cd62087b0e141f75a4`)
.then(({ data }) => {
console.log(data);
dispatch({
type: FETCH_ITEM_SUCCESS,
data,
});
})
.catch((error) => {
dispatch({
type: FETCH_ITEM_FAILURE,
});
});
};
Then, I try to set item field in State in my reducer:
const initialState = {
isAuthenticated: false,
user: {},
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ITEM_REQUEST:
return {
...state,
isLoading: true,
};
case FETCH_ITEM_SUCCESS:
return {
...state,
item: action.data,
isLoading: false,
};
}
};
Then, I try to get those data in Details component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchItemAction } from 'actions/actions';
class Details extends Component {
componentDidMount() {
const { fetchItem } = this.props;
fetchItem();
}
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{/* <p>{item.title}</p> */}
</>
);
}
}
const mapStateToProps = ({ item, isLoading }) => ({ item, isLoading });
const mapDispatchToProps = dispatch => ({
fetchItem: () => dispatch(fetchItemAction()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Details);
As a result, I'm getting following data in console:
Apart from two undefinded the result looks good because there is correct response from my backend.
But, when I try to uncomment <p>item.title</p> line in Details.js, the app crash:
TypeError: Cannot read property 'title' of undefined
I also implemented correctly fetchItemsAction(), addItemAction() and deleteItemAction() which are very similar but I have no idea what is wrong in fetchItemAction().
This is an asynchronous issue. componentDidMount is called when the component is mounted. Then, you're calling fetch. So on your first render, item is undefined. Once the data is returned, the render is triggered again with item data.
So, just check if item is defined:
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{item && <p>{item.title}</p>}
</>
);
}