i am new to context api, I tried updating the context state by sending a dispatch to the reducer, but when i log the state, i get the default state. however when i inspected inside the react dev tools, i found the the state does have a changed state, but it's just not logging it out in the console, am i doing something wrong?
const State = ({ children }) => {
const initState = {
trending: [],
search: []
}
const [state, dispatch] = useReducer(Reducer, initState)
useEffect(() => {
console.log(state.trending) //returns []
dispatch({ type: 'LOAD_TRENDING', payload: ['some Data'] })
console.log(state.trending); // returns [] instead of ['some Data']
},
[])}
You need to put state.trending in the dependency array in the useEffect. So your could would look like:
useEffect(() => {
console.log(state.trending) //returns []
console.log(state.trending); // returns [] instead of ['some Data']
},
[state.trending]) // this is new
}
Also moving the dispatch to a seperate useEffect to avoid infinite rendering.
useEffect(() => {
dispatch({ type: 'LOAD_TRENDING', payload: ['some Data'] })
},
[])}
Related
Using an api for anime called Jikan, I'm trying to display promo thumbnails of new anime shows.
I'm using two api calls, one to get the new anime shows:
export const get_new_anime = () =>
`${base_url}search/anime?q&order_by=score&status=airing&sort=desc`;
and one for getting the videos (containing promos) of anime by getting its id.
export const get_news = (anime_id) => `${base_url}anime/${anime_id}/videos`;
In my home page, here I am mapping the shows, returning a component for each anime:
<Promos>
{new.map((anime, index) => (
<Anime key={anime.mal_id} index={index}></Anime>))}
</Promos>
And for each Anime component, I have a useEffect which uses useDispatch for every new id
const Anime = ({ id, index }) => {
const dispatch = useDispatch();
const loadDetailHandler = () => {
// eslint-disable-line react-hooks/exhaustive-deps
dispatch(loadDetail(id));
useEffect(() => {
loadDetailHandler(id);
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
const promo = useSelector((state) => state.detail.promo);
const isLoading = useSelector((state) => state.detail.isLoading);
return (
<PromoBox
style={
!isLoading
? { backgroundImage: `url("${promo[index][0].image_url}")` }
: null
}
></PromoBox>);
};
Here is how my promoReducer looks like:
const initState = {
promo: [],
isLoading: true,
};
const promoReducer = (state = initState, action) => {
switch (action.type) {
case "LOADING_PROMO":
return {
...state,
isLoading: true,
};
case "GET_DETAIL":
return {
...state,
promo: [...state.promo, action.payload.promo],
isLoading: false,
};
default:
return { ...state };
}
};
export default promoReducer;
and here is the promoAction:
export const loadPromo = (id) => async (dispatch) => {
dispatch({
type: "LOADING_PROMO",
});
const promoData = await axios.get(get_promos(id));
dispatch({
type: "GET_DETAIL",
payload: {
promo: promoData.data.promo,
},
});
};
While it does return the promo data as the action is dispatched, the problem is that in some instances of dispatching, no data is returned. Here is a screenshot from redux devtools to show what I mean:
and I was trying to get the promos of all the new anime, in which I was expecting to get 50 results of promo data. In devtools, you can see I only got 9 of them. This is followed by an error 429 (too many requests):
How can I resolve this issue? And is there a better way to do this, because this seems like bad practice:
Well it seems that you're limited by the api itself and it's threshold for the number of request per unit of time. There should probably be a request that allows you to pass multiple anime ids to get request in order to avoid requesting details for each anime individually.
I'm trying to access a property of an object like this one:
export const ListingDef = {
assistant: {
title: 'a'
},
contract: {
title: 'b'
},
child: {
title: 'c'
}}
I'm using Context and I have a 'listingType' property (empty string) in my state and I define its value in a useEffect hook:
const Listing = ({type}) => {
const {listingType, setListingType} = useContext(ListingContext);
useEffect(() => {
setListingType(type);
}, [])
return (
<>
<h1 className="listing-title">{ListingDef[listingType].title}</h1>
<ListingTable/>
</>
)}
That way, I can access to ListingDef[listingType].title. But I'm facing this issue:
Cannot read property 'title' of undefined
At first render, listingType is equal to empty string so I think I understand why I get this error back.
In other components, I would access to other properties of my object also based on listingType value.
It works if I deal with boolean condition, but is there anyway to avoid if(listingType)... each time I want to access to my object.
(It works fine if I use type directly from the props but I need this value in other components)
Thank you :)
My Context file:
const initialState = {
listingType: '',
listingData: [],
loading: true
}
export const ListingContext = createContext(initialState);
export const ListingProvider = ({children}) => {
const [state, dispatch] = useReducer(ListingReducer, initialState);
const setListingType = (type) => {
dispatch({
type: SET_LISTING_TYPE,
payload: type
})
}
const getListingData = async (listingType) => {
try {
const data = await listObjects(listingType);
dispatch({
type: GET_LISTING_DATA,
payload: data
})
} catch (err) {
}
}
const contextValue = {
setListingType,
getListingData,
listingType: state.listingType,
listingData: state.listingData,
loading: state.loading
}
return (
<ListingContext.Provider value={contextValue}>
{children}
</ListingContext.Provider>
)
}
I am relatively new to redux but i don't see the point of using useEffect when the prop type is already working. Either use a default value or add the 'type' prop to the useEffect second param.
I am attempting to query my Firebase backend through a redux-thunk action, however, when I do so in my initial render using useEffect(), I end up with this error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
My action simply returns a Firebase query snapshot which I then received in my reducer. I use a hook to dispatch my action:
export const useAnswersState = () => {
return {
answers: useSelector(state => selectAnswers(state)),
isAnswersLoading: useSelector(state => selectAnswersLoading(state))
}
}
export const useAnswersDispatch = () => {
const dispatch = useDispatch()
return {
// getAnswersData is a redux-thunk action that returns a firebase snapshot
setAnswers: questionID => dispatch(getAnswersData(questionID))
}
}
and the following selectors to get the data I need from my snapshot and redux states:
export const selectAnswers = state => {
const { snapshot } = state.root.answers
if (snapshot === null) return []
let answers = []
snapshot.docs.map(doc => {
answers.push(doc.data())
})
return answers
}
export const selectAnswersLoading = state => {
return state.root.answers.queryLoading || state.root.answers.snapshot === null
}
In my actual component, I then attempt to first query my backend by dispatching my action, and then I try reading the resulting data once the data is loaded as follows:
const params = useParams() // params.id is just an ID string
const { setAnswers, isAnswersLoading } = useAnswersDispatch()
const { answers } = useAnswersState()
useEffect(() => {
setAnswers(params.id)
}, [])
if (!isAnswersLoading)) console.log(answers)
So to clarify, I am using my useAnswersDispatch to dispatch a redux-thunk action which returns a firebase data snapshot. I then use my useAnswersState hook to access the data once it is loaded. I am trying to dispatch my query in the useEffect of my actual view component, and then display the data using my state hook.
However, when I attempt to print the value of answers, I get the error from above. I would greatly appreciate any help and would be happy to provide any more information if that would help at all, however, I have tested my reducer and the action itself, both of which are working as expected so I believe the problem lies in the files described above.
Try refactoring your action creator so that dispatch is called within the effect. You need to make dispatch dependent on the effect firing.
See related
const setAnswers = (params.id) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(useAnswersDispatch(params.id));
}, [])
}
AssuminggetAnswersData is a selector, the effect will trigger dispatch to your application state, and when you get your response back, your selector getAnswersData selects the fields you want.
I'm not sure where params.id is coming from, but your component is dependent on it to determine an answer from the application state.
After you trigger your dispatch, only the application state is updated, but not the component state. Setting a variable with useDispatch, you have variable reference to the dispatch function of your redux store in the lifecycle of the component.
To answer your question, if you want it to handle multiple dispatches, add params.id and dispatch into the dependencies array in your effect.
// Handle null or undefined param.id
const answers = (param.id) => getAnswersData(param.id);
const dispatch = useDispatch();
useEffect(() => {
if(params.id)
dispatch(useAnswersDispatch(params.id));
}, [params.id, dispatch]);
console.log(answers);
As commented; I think your actual code that infinite loops has a dependency on setAnswers. In your question you forgot to add this dependency but code below shows how you can prevent setAnswers to change and cause an infinite loop:
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
// const dispatch = useDispatch(); //react-redux useDispatch will never change
//never re create setAnswers because it causes the
// effect to run again since it is a dependency of your effect
const setAnswers = React.useCallback(
questionID => dispatch(getAnswersData(questionID)),
//your linter may complain because it doesn't know
// useDispatch always returns the same dispatch function
[dispatch]
);
return {
setAnswers,
};
};
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is your original code causing infinite loop because setAnswers keeps changing.
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
return {
//re creating setAnswers, calling this will cause
// state.data to be set causing Data to re render
// and because setAnser has changed it'll cause the
// effect to re run and setAnswers to be called ...
setAnswers: questionID =>
dispatch(getAnswersData(questionID)),
};
};
let timesRedered = 0;
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
//securit to prevent infinite loop
timesRedered++;
if (timesRedered > 20) {
throw new Error('infinite loop');
}
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You just need to add params.id as a dependency.
Don't dispatch inside the function which you are calling inside useEffect but call another useEffect to dispatch
const [yourData, setyourData] = useState({});
useEffect(() => {
GetYourData();
}, []);
useEffect(() => {
if (yourData) {
//call dispatch action
dispatch(setDatatoRedux(yourData));
}
}, [yourData]);
const GetYourData= () => {
fetch('https://reactnative.dev/movies.json')
.then((response) => response.json())
.then((json) => {
if (result?.success == 1) {
setyourData(result);
}
})
.catch((error) => {
console.error(error);
});
};
I'm using React and Redux and storing data in a loggedUser variable upon user login.
my login reducer looks like this:
const loginReducer = (state = null, action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};
export const fetchUser = () => {
return dispatch => {
const userStr = window.localStorage.getItem("loggedVintageUser");
const user = JSON.parse(userStr);
if (user) {
dispatch({ type: "SET_USER", data: user });
}
};
};
export const setUser = data => {
return dispatch => {
dispatch({ type: "SET_USER", data });
};
};
export const login = data => {
return async dispatch => {
const user = await loginService.login({
username: data.username,
password: data.password
});
window.localStorage.setItem("loggedVintageUser", JSON.stringify(user));
dispatch({ type: "SET_USER", data: user });
};
};
In my core App component i'm dispatching the fetchUser and setUser creators
useEffect(() => {
fetchUser();
}, [props.fetchUser]);
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem("loggedVintageUser");
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON);
props.setUser(user);
userService.setToken(user.token);
}
}, []);
I'm displaying a list of favorite items for a user and when i go to refresh the page, i'm getting the following error:
TypeError: Cannot read property 'favorites' of null
Here is relevant code for my Favorites component. The error is triggered on the loggedUser.favorites data. I can see when visiting the favorites page, the loggedUser field is there and data displays fine but on refresh the loggedUser variable turns to null.
const searchCards = ({ loggedUser, search }) => {
const favorites = loggedUser.favorites;
console.log("FAVORITES", favorites);
return search
? favorites.filter(a =>
a.title
.toString()
.toLowerCase()
.includes(search.toLowerCase())
)
: favorites;
};
const Cards = props => {
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const [filteredData, setData] = useState(props.cardsToShow);
const mapStateToProps = state => {
return {
baseball: state.baseball,
loggedUser: state.loggedUser,
page: state.page,
entries: state.entries,
query: state.query,
pageOutput: state.pageOutput,
search: state.search,
cardsToShow: searchCards(state)
};
};
const mapDispatchToProps = {
searchChange,
fetchData,
updateUser
};
I tried to add this before i render the data, but it's not working
if (!props.loggedUser) return null;
How can i retain that state if a user is refreshing the page. The odd part is that on my home page where i have a similar sort of display a refresh isn't causing the same problems.
check once loggedUser is exist in state or not. Print state using console.log(state). you may also open inspect tool and go to application tab and click on local storage, you will get localStorage data.
Well, i figured this out and got some help from this post here. Redux store changes when reload page
My loggedUser state was disappearing after reload, so i just loaded the inital state for loggedUser pulling the data from the local storage:
function initState() {
return {
token: localStorage.token,
firstName: localStorage.firstName,
id: localStorage.id,
favorites: localStorage.favorites,
username: localStorage.username
};
}
const loginReducer = (state = initState(), action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};
I am new to ReactJS and working with Hooks. I want to set state for object. Here is my code:
const StartPage = (props)=> {
const [user,setUser] = useState('')
const [password,setPassword] = useState('')
const [info,setInfo] = useState({
email:''
]})
const onUserChange=(e)=>{
const user = e.target.value
setUser(user)
}
const onPasswordChange=(e)=>{
const password = e.target.value
setPassword(password)
}
const onSubmit=(e)=>{
e.preventDefault()
const myHeader = new Headers ({
'APIKey':'*****',
"Content-Type": "application/json",
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers':'headers'
})
fetch(`[my_link_to_api]?username=${user}&password=${password}`,{
method:'GET',
credentials: 'omit',
headers:myHeader,
})
.then(response =>response.json())
.then(data =>{
if(data!==undefined){
setInfo({...info, data})
}
console.log(info)
})
.then(()=>{
console.log(info)
history.push({
pathname:'/dashboard',
state:info
})
})
.catch((e)=>{
console.log(e)
})
}
return (
<div>
<form onSubmit={onSubmit}>
<input type="text" placeholder="username" onChange={onUserChange}></input>
<input type="password" placeholder="password" onChange={onPasswordChange}></input>
<button>Login</button>
</form>
</div>
)
}
The problem part is here:
I declare object initial state here:
const [info,setInfo] = useState({
email:''
})
And here:
.then(data =>{
if(data!==undefined){
setInfo({...info, data})
}
console.log(info)
})
it is just not working. It is not setting info and data in never written. How can I assign the received data (because I console logged it and it it received) to the info?
setInfo is asynchronous and you won't see the updated state until the next render, so when you do this:
if(data!==undefined){
setInfo({...info, data })
}
console.log(info)
you will see info before the state was updated.
You can use useEffect hook (which tells React that your component needs to do something after render) to see the new value of info:
const StartPage = props => {
const [user, setUser] = useState('');
...
useEffect(() => {
console.log(info);
}, [info]);
...
}
EDIT
Also as others have pointed out, you likely want to destructure data when setting the state: setInfo({...info, ...data }) (this entirely depends on how you are planning to use it), otherwise the state will look like this:
{
email: ...
data: ...
}
What is the structure of data received from the api call? you can try setting it like so -
setInfo({ ...info, email: data.email })
I'm not exactly sure what you mean by "not working" but it looks like you are deconstructing info but not data. My suspicion is that it is setting state but not the way you want. If data is an object with email as a property you most likely will want to deconstruct that too like this:
setInfo({...info, ...data})
You can use useEffect to console.log state changes if you want with something like this:
import React, { useState, useEffect } from 'react'
const StartPage = props => {
const [info,setInfo] = useState({ email:'' })
useEffect(() => {
console.log(info)
}, [info])