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>
);
};
Related
I am trying to learn state management with the useReducer hook so I have built a simple app that calls the pokeAPI. The app should display a random pokemon, and add more pokemons to the screen as the 'capture another' button is pressed.
However, it rerenders the component with the initialized and empty Card object before populating the Card from the axios call. I've tried at least 3 different solutions based on posts from stackoverflow.
In each attempt I have gotten the same result: the app displays an undefined card on, even though the state is updated and not undefined, it just was updated slightly after the rerendering. When clicked again that prior undefined gets properly rendered but there is now a new card displayed as undefined.
I am still getting the hang of react hooks (no pun intended!), async programming, and JS in general.
Here is the app:
https://stackblitz.com/edit/react-ts-mswxjv?file=index.tsx
Here is the code from my first try:
//index.tsx
const getRandomPokemon = (): Card => {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
//omitted for brevity
})
.catch((error) => {
//omitted
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
//omitted
})
.catch((error) => {
//omitted
});
return newCard;
};
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function addCard() {
let newCard: Card = getRandomPokemon();
dispatch({
type: ActionKind.Add,
payload: newCard,
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={addCard}>Catch Another</CatchButton>
</div>
);
};
//cardReducer.tsx
export function cardReducer(state: Card[], action: Action): Card[] {
switch (action.type) {
case ActionKind.Add: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
clonedState = [...clonedState, action.payload];
return clonedState;
}
default: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
return clonedState;
}
}
}
//Deck.tsx
//PokeDeck and PokeCard are styled-components for a ul and li
export const Deck = ({ deck }: DeckProps) => {
useEffect(() => {
console.log(`useEffect called in Deck`);
}, deck);
return (
<PokeDeck>
{deck.map((card) => (
<PokeCard>
<img src={card.image} alt={`image of ${card.name}`} />
<h2>{card.name}</h2>
</PokeCard>
))}
</PokeDeck>
);
};
I also experimented with making the function that calls Axios a promise so I could chain the dispatch call with a .then.
//index.tsx
function pokemonPromise(): Promise<Card> {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
return new Promise((resolve) => {
resolve(newCard);
});
}
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function asyncAdd() {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
console.log(newCard);
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={asyncAdd}>Catch Another</CatchButton>
</div>
);
};
I also tried to have it call it with a side effect using useEffect hook
//App.tsx
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={()=>{setCatchCount(catchCount + 1)}>Catch Another</CatchButton>
</div>
);
};
So there are a couple of things with your code, but the last version is closest to being correct. Generally you want promise calls inside useEffect. If you want it to be called once, use an empty [] dependency array. https://reactjs.org/docs/hooks-effect.html (ctrl+f "once" and read the note, it's not that visible). Anytime the dep array changes, the code will be run.
Note: you'll have to change the calls to the Pokemon service as you're running two async calls without awaiting either of them. You need to make getRandomPokemon async and await both calls, then return the result you want. (Also you're returning newCard but not assigning anything to it in the call). First test this by returning a fake data in a promise like my sample code then integrate the api if you're having issues.
In your promise, it returns a Card which you can use directly in the dispatch (from the response, you don't need the extra step). Your onclick is also incorrectly written with the brackets. Here's some sample code that I've written and seems to work (with placeholder functions):
type Card = { no: number };
function someDataFetch(): Promise<void> {
return new Promise((resolve) => setTimeout(() => resolve(), 1000));
}
async function pokemonPromise(count: number): Promise<Card> {
await someDataFetch();
console.log("done first fetch");
await someDataFetch();
console.log("done second fetch");
return new Promise((resolve) =>
setTimeout(() => resolve({ no: count }), 1000)
);
}
const initialState = { name: "pikachu" };
const cardReducer = (
state: typeof initialState,
action: { type: string; payload: Card }
) => {
return { ...state, name: `pikachu:${action.payload.no}` };
};
//App.tsx
const App = () => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
pokemonPromise(catchCount)
.then((newCard) => {
dispatch({
type: "ActionKind.Add",
payload: newCard
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
{deck.name}
<button onClick={() => setCatchCount(catchCount + 1)}>
Catch Another
</button>
</div>
);
};
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.
I've created a custom hook to fetch data with events handlers, when I using it on click event the hook makes the request on the second click
useFetch.js
import { useState, useEffect } from 'react';
import { makeRequest } from '../utils';
const useFetch = (query = {}) => {
const [request, setRequest] = useState({ ...query });
const [data, setData] = useState({
response: null,
isError: null,
isLoading: request.isLoading,
});
const fetchData = async () => {
if (!request.url) {
return;
}
try {
const res = await makeRequest(
request.url,
request.method || 'get',
request.body || null,
);
setData({
response: res,
isLoading: false,
error: null,
});
} catch (error) {
setData({
response: null,
error,
isLoading: false,
});
}
};
const onEvent = (req) => {
if (req) {
setRequest({ ...req });
}
};
useEffect(() => fetchData(), [request]);
return { ...data, onEvent };
};
export default useFetch;
Component File
const { isLoading, isError, response, onEvent } = useFetch();
const ClickMe = () => {
onEvent({
url: 'v1/profile/login',
method: 'post',
body: {
username: 'eee#ddd.com',
password: '2342332',
},
});
console.log('response', response);
};
return (
<>
<button onClick={() => ClickMe()} type="button">
Click Me
</button>
)
the log inside the ClickMe function is null in the first click but in the second click it returns the value
Because fetchData is asynchronous function you cannot know when resposne will be set, that's why you cannot access it like normal sync code
in your app code you could observe response change to console it like
useEffect(() => { console.log(response) }, [ response ]);
At the time of console.log, the response is not fetched. Since when ever response changes, the component re-renders, you can try like below to see the updated values of isLoading and response.
return (
<>
{isLoading && <div> Loading... </div>}
{`Resonse is ${JSON.stringify(response)}`}
<button onClick={() => ClickMe()} type="button">
Click Me
</button>
</>
);
As the others said, it's an asynchronous operation. If you want to use the response as soon as you called onEvent, you can do something along these lines using a promise :
import { useState, useEffect } from 'react';
import { makeRequest } from '../utils';
const useFetch = (query = {}) => {
useEffect(() => {
if (query) {
fetchData(query)
}
}, []) // if query is provided, run query
const [data, setData] = useState({
response: null,
isError: null,
isLoading: true
});
const fetchData = async (query) => {
return new Promise((resolve, reject) => {
if (!query.url) {
reject('url needed')
}
makeRequest(query).then(res => {
setData({
response: res,
isLoading: false,
error: null
})
resolve(res)
).catch(error => {
setData({
response: null,
error,
isLoading: false
});
reject(error)
});
})
})
};
// provide fetchData directly for lazy calls
return { ...data, fetchData };
};
export default useFetch;
And then call it like so :
const { response, fetchData } = useFetch()
fetchData({
url: 'v1/profile/login',
method: 'post',
body: {
username: 'eee#ddd.com',
password: '2342332',
},
}).then(res => ...);
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>}
</>
);
}