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>
);
};
Related
I am making a request to my API from the client. I set the information in a React state. The weird thing is the following:
When I print the result of the fetch by console I get this:
But when I display to see in detail, specifically the "explore" array, I get that the result is empty:
The explore array is passed to two components by props to show a list, the super weird thing is that one of the components shows the information but the other does not
Fetch
const baseUrl = process.env.BASE_API_URL;
import { marketPlace } from "./mock";
import { requestOptions } from "utils/auxiliaryFunctions";
const getInitialMarketPlaceData = async (username: string) => {
try {
return await fetch(
`${baseUrl}marketplace/home/${username}`,
requestOptions()
)
.then((data) => data.json())
.then((res) => {
console.log(res.data)
if (res.success) {
return res.data;
}
});
} catch (err) {
throw err;
}
};
Components
export default function Marketplace() {
const [marketPlace, setMarketPlace] = useState<any>();
const [coachData, setCoachData] = useState<false | any>();
const [coach, setCoach] = useState<string>();
const { user } = useContext(AuthContext);
useEffect(() => {
if (!marketPlace) {
getInitialMarketPlaceData(user.username).then((data) => {
console.log("data", data);
setMarketPlace(data);
});
}
}, []);
useEffect(() => {
console.log("marketplace", marketPlace);
}, [marketPlace]);
useEffect(() => {
console.log("coachData", coachData);
}, [coachData]);
const changeCoachData = async (coach: string) => {
setCoach(coach);
let res = await getCoachProfile(coach);
setCoachData(res);
};
if (!marketPlace) {
return <AppLoader />;
}
return (
<main className={styles.marketplace}>
{marketPlace && (
// Highlited viene vacĂo de la API
<Highlited
highlitedCoaches={/*marketPlace.highlighted*/ marketPlace.explore}
></Highlited>
)}
{marketPlace.favorites.length !== 0 && (
<Favorites
changeCoachData={changeCoachData}
favCoaches={marketPlace.favorites}
></Favorites>
)}
{
<Explore
changeCoachData={changeCoachData}
exploreCoaches={marketPlace.explore}
></Explore>
}
{/*<Opinions
changeCoachData={changeCoachData}
opinions={marketPlace.opinions}
></Opinions>*/}
{coachData && (
<CoachProfileRookieView
coachData={coachData}
coachUsername={coach}
isCoach={false}
isPreview={false}
onClose={() => setCoachData(false)}
/>
)}
</main>
);
}
I'm new in react hooks, here converting from class(class version works) to hooks, i'm not sure if i have done it correctly because when using 'then' in hooks it says 'Property 'then' does not exist on type '(dispatch: any) => Promise'.ts(2339)'
this is class version which works:
import {
getGraph,
getFloorplan,
changeActiveCamera,
} from "../redux/actions";
const mapStateToProps = (state) => {
return {
currentSite: state.selection.currentSite,
currentCamera: state.selection.currentCamera,
};
};
function mapDispatchToProps(dispatch) {
return {
getGraph: (site) => dispatch(getGraph(site)),
getFloorplan: (site) => dispatch(getFloorplan(site)),
changeActiveCamera: (site, id) => dispatch(changeActiveCamera(site, id)),
};
}
loadGraph() {
if (this.props.currentSite) {
this.props.getFloorplan(this.props.currentSite.identif).then(() => {
console.log("Fetched floorplan");
this.props.getGraph(this.props.currentSite.identif).then(() => {
console.log("Fetched model", this.props.realGraph.model);
// new camera-related node & link status
if (this.props.currentCamera) {
this.props.changeActiveCamera(
this.props.currentSite.identif,
this.props.currentCamera.identif
);
}
});
});
}
}
this is what i have done in order to convert it :
const dispatch = useDispatch();
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const loadGraph = () => {
if (currentSite) {
dispatch(getFloorplan(currentSite.identif)).then(() => {
console.log("Fetched floorplan");
dispatch(getGraph(currentSite.identif)).then(() => {
console.log("Fetched model", realGraph.model);
// new camera-related node & link status
if (currentCamera) {
dispatch(
changeActiveCamera(
currentSite.identif,
currentCamera.identif
)
);
}
});
});
}
};
After seeing video shared in comment, i changed the code and getting new error: 'Uncaught RangeError: Maximum call stack size exceeded
at getFloorplan '
my code:
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const getFloorplan = (site) => dispatch(getFloorplan(site));
const getGraph = (site) => dispatch(getGraph(site));
const changeActiveCamera = (site, id) =>
dispatch(changeActiveCamera(site, id));
const loadGraph = () => {
if (currentSite) {
getFloorplan(currentSite.identif).then(() => {
console.log("Fetched floorplan");
getGraph(currentSite.identif).then(() => {
console.log("Fetched model", realGraph.model);
// new camera-related node & link status
if (currentCamera) {
changeActiveCamera(
currentSite.identif,
currentCamera.identif
);
}
});
});
}
};
I highly doubt this simple solution will work, but here is my suggestions for refactors.
Please note that the way that your old class was written, that's not the recommended way of writing redux. Sure, the code will work, but it's not the "right" way in many people's opinions.
Refactor your actions
I would rather have my code written out like this :
YourComponent.tsx
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const dispatch = useDispatch();
const loadGraph = () => {
useEffect(() => {
if(currentSite)
getFloorPlan(currenSite.identif);
}, [ dispatch, currentSite.identif])
// dispatch is in the dependency array to stop make eslinter complain, you can remove it if you want, others are used to control the number of renders
};
YourComponentActions.ts
// assuming you are using redux-thunk here
const getFloorplan = ( site : YourType ) => async dispatch => {
console.log("Fetched floorplan");
// remove await if getImage is a sync function, try with await first and remove if it doesn't fit
const response = await getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
dispatch(getGraph(response))
}
const getGraph = (site : YourType) => async dispatch => {
console.log("Fetched model", realGraph.model);
// new camera-related node & link status
if (currentCamera) {
changeActiveCamera(currentSite.identif,currentCamera.identif);
}
}
const changeActiveCamera = ( site: YourType, param: YourAnotherType ) => async dispatch => {
// your logic here
}
I have a react component that has a html button that when clicked calls a function that adds an element to a redux reducer and then redirects to another component. The component that is redirected to needs to set state from the reducer but it won't. I know that it is being added to the array in the reducer because I wrote it as an async await and it redirects after it gets added.
This is the original component
const Posts = () => {
const dispatch = useDispatch();
const getProfile = async (member) => {
await dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
window.location.href='/member'
console.log('----------- member------------')
console.log(post)
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default Posts;
This is the 'socialNetworkContract' reducer
import { connect, useDispatch, useSelector } from "react-redux";
let init = {
posts:[],
post:{},
profiles:[],
profile:{},
members:[],
member:{}
}
export const socialNetworkContract = (state = init, action) => {
const { type, response } = action;
switch (type) {
case 'ADD_POST':
return {
...state,
posts: [...state.posts, response]
}
case 'SET_POST':
return {
...state,
post: response
}
case 'ADD_PROFILE':
return {
...state,
profiles: [...state.profiles, response]
}
case 'SET_PROFILE':
return {
...state,
profile: response
}
case 'ADD_MEMBER':
return {
...state,
members: [...state.members, response]
}
case 'SET_MEMBER':
return {
...state,
member: response
}
default: return state
}
};
and this is the component that the html button redirects to
const Member = () => {
const [user, setUser] = useState({})
const [profile, setProfile] = useState({});
const dispatch = useDispatch();
useEffect(async()=>{
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div class="container">
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
{p.message}
{p.replies}
</tr>})}
</div>
</div>
</div>
</div>
)
}
export default Member;
This is the error I get in the console
Error: invalid address (arg="user", coderType="address", value={})
The functions I'm calling are solidity smart contracts and the have been tested and are working and the element I'm trying to retrieve out of the array is an ethereum address.
incidentsInstance and snInstance are declared in the try statement but I took a lot of the code out to make it easier to understand.
given setUser is async, your user is still an empty object when you make your request.
you could pass pro value instead:
useEffect(async () => {
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
const a = await snInstance.getUsersPosts(pro, { from: accounts[0] });
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
or break your useEffect in two pieces:
useEffect(() => {
setUser(socialNetworkContract.members[0]);
}, [])
useEffect(async () => {
if (!Object.keys(user).length) return;
try {
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [user])
note: fwiw, at first sight your user state looks redundant since it's derived from a calculated value.
As you can see below in the dev tools screen shot, the child element does have props. My issue is I cannot get them to appear in the DOM when the component is first rendered. I have to click on the Link element again to re-render the component and only then does the map function work correctly (second screen shot). Another thing is I am using the same code in another component and it works fine. Help!
import React, { useState, useEffect } from 'react'
import firebase from 'firebase';
import NewsLetterListChildComponent from './children/NewsLetterListChildComponent';
import LoadingComponent from '../Loading/LoadingComponent';
function PublicNewsLetterListComponent({ user }) {
const [ newsLetters, setNewsLetters ] = useState([]);
const [ loading, setLoading ] = useState(false);
const [ errors, setErrors ] = useState(false);
useEffect(() => {
let requestCancelled = false;
const getNewsLetters = () => {
setLoading(true);
let newsLetterArray = [];
firebase
.firestore()
.collection('newsLetters')
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
const listRef = firebase.storage().ref().child('newsLetterImagesRef/' + doc.id);
listRef
.getDownloadURL()
.then(url => {
newsLetterArray.push({ id: doc.id, data: doc.data(), image: url });
})
.catch(error => console.log(error))
});
});
setNewsLetters(newsLetterArray);
setLoading(false);
};
getNewsLetters();
return () => {
requestCancelled = true;
};
}, []);
const renderContent = () => {
if(loading) {
return <LoadingComponent />
} else {
return <NewsLetterListChildComponent newsLetters={newsLetters} />
}
}
return renderContent();
}
export default PublicNewsLetterListComponent
import React from 'react';
import { ListGroup, ListGroupItem, Row, Col } from 'reactstrap';
function NewsLetterListChildComponent({ newsLetters }) {
return (
<div>
<Row>
<Col md={{size: 6, offset: 3}}>
<ListGroup>
{newsLetters.map((item, index) => {
return (
<ListGroupItem key={index} className="m-1" ><h1>{item.data.title} </h1><img src={item.image} alt={item.data.title} className="thumb-size img-thumbnail float-right" /></ListGroupItem>
);
})}
</ListGroup>
</Col>
</Row>
</div>
)
}
export default NewsLetterListChildComponent;
Initial render and the list group is empty
after the re-render and now the list group is populated
You need to call setNewsLetters when the data is resolved:
const getNewsLetters = async () => {
setLoading(true);
try {
const newsLetters = await firebase
.firestore()
.collection("newsLetters")
.get();
const data = await Promise.all(
newsLetters.docs.map(async (doc) => {
const url = await firebase
.storage()
.ref()
.child("newsLetterImagesRef/" + doc.id)
.getDownloadURL();
return {
id: doc.id,
data: doc.data(),
image: url,
};
})
);
setNewsLetters(data);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
The useEffect code contains an async request and you are trying to update an array of newsLetters in state even before it will be fetched. Make use of Promise.all and update the data when it is available
useEffect(() => {
let requestCancelled = false;
const getNewsLetters = () => {
setLoading(true);
firebase
.firestore()
.collection('newsLetters')
.get()
.then((querySnapshot) => {
const promises = querySnapshot.map((doc) => {
const listRef = firebase.storage().ref().child('newsLetterImagesRef/' + doc.id);
return listRef
.getDownloadURL()
.then(url => {
return { id: doc.id, data: doc.data(), image: url };
})
.catch(error => console.log(error))
Promise.all(promises).then(newsLetterArray => { setNewsLetters(newsLetterArray);})
});
});
setLoading(false);
};
getNewsLetters();
return () => {
requestCancelled = true;
};
}, []);
If you check newletters with if, your problem will most likely be resolved.
review for detail : https://www.debuggr.io/react-map-of-undefined/
if (newLetters){
newLetters.map(item=> ...)
}
I'm newbie in React but I'm developing an app which loads some data from the server when user open the app. App.js render this AllEvents.js component:
const AllEvents = function ({ id, go, fetchedUser }) {
const [popout, setPopout] = useState(<ScreenSpinner className="preloader" size="large" />)
const [events, setEvents] = useState([])
const [searchQuery, setSearchQuery] = useState('')
const [pageNumber, setPageNumber] = useState(1)
useEvents(setEvents, setPopout) // get events on the main page
useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber)
// for ajax pagination
const handleSearch = (searchQuery) => {
setSearchQuery(searchQuery)
setPageNumber(1)
}
return(
<Panel id={id}>
<PanelHeader>Events around you</PanelHeader>
<FixedLayout vertical="top">
<Search onChange={handleSearch} />
</FixedLayout>
{popout}
{
<List id="event-list">
{
events.length > 0
?
events.map((event, i) => <EventListItem key={event.id} id={event.id} title={event.title} />)
:
<InfoMessages type="no-events" />
}
</List>
}
</Panel>
)
}
export default AllEvents
useEvents() is a custom hook in EventServerHooks.js file. EventServerHooks is designed for incapsulating different ajax requests. (Like a helper file to make AllEvents.js cleaner) Here it is:
function useEvents(setEvents, setPopout) {
useEffect(() => {
axios.get("https://server.ru/events")
.then(
(response) => {
console.log(response)
console.log(new Date())
setEvents(response.data.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
)
}, [])
return null
}
function useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber) {
useEffect(() => {
setPopout(<ScreenSpinner className="preloader" size="large" />)
let cancel
axios({
method: 'GET',
url: "https://server.ru/events",
params: {q: searchQuery, page: pageNumber},
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(
(response) => {
setEvents(response.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
).catch(
e => {
if (axios.isCancel(e)) return
}
)
return () => cancel()
}, [searchQuery, pageNumber])
return null
}
export { useEvents, useSearchedEvents }
And here is the small component InfoMessages from the first code listing, which display message "No results" if events array is empty:
const InfoMessages = props => {
switch (props.type) {
case 'no-events':
{console.log(new Date())}
return <Div className="no-events">No results :(</Div>
default:
return ''
}
}
export default InfoMessages
So my problem is that events periodically loads and periodically don't after app opened. As you can see in the code I put console log in useEvents() and in InfoMessages so when it's displayed it looks like this:
logs if events are displayed, and the app itself
And if it's not displayed it looks like this: logs if events are not displayed, and the app itself
I must note that data from the server is loaded perfectly in both cases, so I have totally no idea why it behaves differently with the same code. What am I missing?
Do not pass a hook to a custom hook: custom hooks are supposed to be decoupled from a specific component and possibly reused. In addition, your custom hooks return always null and that's wrong. But your code is pretty easy to fix.
In your main component you can fetch data with a custom hook and also get the loading state like this, for example:
function Events () {
const [events, loadingEvents] = useEvents([])
return loadingEvents ? <EventsSpinner /> : <div>{events.map(e => <Event key={e.id} title={e.title} />}</div>
}
In your custom hook you should return the internal state. For example:
function useEvents(initialState) {
const [events, setEvents] = useState(initialState)
const [loading, setLoading] = useState(true)
useEffect(function() {
axios.get("https://server.ru/events")
.then(
(res) => {
setEvents(res.data)
setLoading(false)
}
)
}, [])
return [events, loading]
}
In this example, the custom hook returns an array because we need two values, but you could also return an object with two key/value pairs. Or a simple variable (for example only the events array, if you didn't want the loading state), then use it like this:
const events = useEvents([])
This is another example that you can use, creating a custom hook that performs the task of fetching the information
export const useFetch = (_url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(true);
useEffect(function() {
setLoading('procesando...');
setData(null);
setError(null);
const source = axios.CancelToken.source();
setTimeout( () => {
axios.get( _url,{cancelToken: source.token})
.then(
(res) => {
setLoading(false);
console.log(res.data);
//setData(res);
res.data && setData(res.data);
// res.content && setData(res.content);
})
.catch(err =>{
setLoading(false);
setError('si un error ocurre...');
})
},1000)
return ()=>{
source.cancel();
}
}, [_url])