React - Access to an object property which is settled in useEffect hook - javascript

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.

Related

The useContext state value doesn't change

I changed the value of setEvents and setNextStartHo in useEffect and printed it out in console.log, but it shows the initial value.
I created a Context called EventTimeContext. I want to change nextStartHour and event values ​​in other components.
type Props = {
children: React.ReactNode;
};
export const EventTimeContext = createContext<any>({
nextStartHour: 24,
event: false,
setNextStartHo: (nextStartHour: number) => {},
setEvents: (event: boolean) => {},
})
export const EventTimeContextProvider = ({ children }: Props) => {
const [nextStartHour, setNextStartHour] = useState(24);
const [event, setEvent] = useState<boolean>(false);
const setEvents = useCallback(
(event: boolean) => {
setEvent(event);
},
[setEvent]
);
const setNextStartHo = useCallback(
(hour: number) => {
setNextStartHour(hour);
},
[setNextStartHour]
);
return (
<EventTimeContext.Provider
value={{
nextStartHour,
event,
setEvents: setEvents,
setNextStartHo: setNextStartHo,
}}
>
{children}
</EventTimeContext.Provider>
);
}
In useEffect, I changed the value with the events and setNextStartHo function, but if I take a look at the console.log, it has the initial value.
// index.ts
const { nextStartHour, event, setEvents, setNextStartHo } = useContext(EventTimeContext);
useEffect(() => {
setEvents(true);
setNextStartHo(10);
}, [])
console.log(nextStartHour); // 24
console.log(event); // false
How should I solve the problem?
The problem is that you have the console.log(nextStartHour); // 24 call just in the body of your index.js file, so the value you get when you do your console log comes from BEFORE the rerender caused by useEffect. Your value should be changed, but it won't be clear from a console.log that's just in the main body.
If you want to see this, a good way would be to have a component with this render:
render(
<>
{nextStartHour} // this will show your 10 value if your useEffect code is in this comp.
</>
)
Keep in mind that react is not the same as a straight JS/TS file. It's all about rendering. If you want a more specific example, we'll need more code--but this is what's wrong.

Getting stale value of local state variable after response returned from promise

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.

React redux state is undefined on first render despite of initlizedState

Redux state is undefined in first render and I returned the initilizedState = {} in my reducer
store.js
const store = createStore(
rootReducer,
compose(
applyMiddleware(thunk),
window.devToolsExtension ? window.devToolsExtension() : (f) => f
)
)
export default store
rootReducer.js
const rootReducer = combineReducers({
search: searchReducer,
product: productReducer,
})
export default rootReducer
reducer.js
const initialState = {}
const productReducer = (state = initialState, action) => {
const { type, payload } = action
switch (type) {
case PRODUCTS_ALL:
console.log('reducer')
return { ...state, items: payload }
default:
return state
}
}
export default productReducer
action.js
const products = axios.create({
baseURL: 'http://localhost:8001/api/products',
})
export const allProducts = () => async (dispatch) => {
console.log('fetching')
await products.get('/').then((res) => {
dispatch({
type: PRODUCTS_ALL,
payload: res.data,
})
})
}
And although I used connect() in my feed container
Feed.js
function Feed({ allProducts, product }) {
const [productItems, setProductItems] = useState()
const [loading, setLoading] = useState(false)
React.useEffect(() => {
allProducts()
console.log(product)
}, [])
return (
<div className='feed__content'>
{loading ? (
<Loader
type='Oval'
color='#212121'
height={100}
width={100}
/>
) : (
<div className='feed__products'>
<div className='feed__productsList'>
{product.map((product) => {
return (
<Product
name={product.name}
image={product.image}
price={product.price}
/>
)
})}
</div>
</div>
)}
</div>
)
}
const mapStateToProps = (state) => ({
product: state.product.items,
})
const mapDispatchToProps = (dispatch) => {
return {
allProducts: () => dispatch(allProducts()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Feed)
if you look at the data on the console you will see undefined but if I add a useEffect dependency it will create a loop , and the first of them is undefined and the rest are the data that I want.
because of this problem when I want to render products with map , it throws an error that said can't map undefiend .
How can I solve this problem
Joel Jaimon code worked well .
and in my code I added
const initialState = {
items: []
}
according to #Andrew and product?.map so my code works well now.
In Feed.js, you aren't getting the entire slice of state. You are trying to access the key item inside.
const mapStateToProps = (state) => ({
product: state.product.items,
})
Change your initialState to include that key and your code should be fine
const initialState = {
items: []
}
A/c to your code you're trying to make an async action in redux. You should use redux-saga or redux-thunk for this.
But you can achieve your output in the following way, without using redux-saga or thunk.
Modify your code in the following way:
Feed.js
function Feed({ allProducts, product }) {
// Set loading to true inititally
const [loading, setLoading] = useState(true);
React.useEffect(() => {
// Make your api call here once its complete and you get a res,
// dispatch the referred action with response as payload.
(async () => {
const products = axios.create({
baseURL: "http://localhost:8001/api/products",
});
const {data} = await products.get("/");
allProducts(data);
})()
// This call is only made when you load it for the first time, if it
// depends
// on something add that dependencies in the dependency array of //
// useEffect.
}, []);
// once store is updated with the passed payload. Set loading to
// false.
React.useEffect(() => {
if(product){
setLoading(false);
}
}, [product])
return (
<div className="feed__content">
{loading ? (
<Loader type="Oval" color="#212121" height={100} width={100} />
) : (
<div className="feed__products">
<div className="feed__productsList">
{product.map((product) => {
return (
<Product
name={product.name}
image={product.image}
price={product.price}
/>
);
})}
</div>
</div>
)}
</div>
);
}
const mapStateToProps = ({product}) => ({
product: product?.items,
});
// here we have a payload to pass
const mapDispatchToProps = (dispatch) => {
return {
allProducts: (payload) => dispatch(allProducts(payload)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Feed);
Action
export const allProducts = (payload) => ({
type: "PRODUCTS_ALL",
payload,
});
Reducer
const productReducer = (state = initialState, action) => {
const { type, payload } = action
switch (type) {
case "PRODUCTS_ALL":
return { ...state, items: payload }
default:
return state
}
}
export default productReducer
Problem statement: I would be giving you three really best methods, in order to solve it, the problem appears when redux is transferring states to the components, so it seems that the render is faster than the response of props to the component.
So, when you have props undefined by their value your application crashes.
note: I will be taking a general example and will show you how to avoid this error.
For example, I have a component in which I want to render the data from the redux response (with mapPropsToState), what I need to do is only to check it for undefined and then if it is undefined we need to use the conditional (ternary) operator for if and else statements.
//suppose you will get an address object from api ,which will contain city ,area ,street ,pin code etc
// address={} intially address is a blank object
render(){
return(
(typeof address.area!='undefined')?
<div>
<div>{address.city}</div>
<div>{address.street}</div>
<div>{address.pin_code}</div>
<div>{address.area}</div>
</div>
:<div>loading....</div>
)
}
And another way is to simply use the package of loadash in loadash you can achieve the same by calling the function "isUndefined"
you can also use it in the inputFields likewise
<FloatingLabel label="Username">
<Form.Control
type="input"
placeholder=" "
name="username"
value={props.username}
defaultValue={_.isUndefined(props.userEditResponse)?'':props.userEditResponse.username}
required
onChange={(e)=>onInputchange(e)}
/>
</FloatingLabel>
//note: you can also use (typeof props.userEditResponse!='undefined')?'do somthing':'leave it'
loadash installation:
npm i lodash
then in your component import the the below line and then you can use it, the way i used in the above example.
import _ from 'lodash'
note: the answer was just to give you an idea and this was really a panic problem that is why I shared it with you guys, it might help many of you, you can use a similar way in different situations. thanks!
furtherMore your can work with ?.:
The ?. operator is like the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined. When used with function calls, it returns undefined if the given function does not exist.
This results in shorter and simpler expressions when accessing chained properties when the possibility exists that a reference may be missing. It can also be helpful while exploring the content of an object when there's no known guarantee as to which properties are required.
{ props.rolesListSuccessResponse?.permissions.map((list,index) => (
<div className="mb-3 col-md-3" key={index}>
<Form.Check
type={'checkbox'}
id={list.name}
label={list.name.charAt(0).toUpperCase()+list.name.slice(1)}
/>
</div>
))}

context state does not update

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'] })
},
[])}

infinite loop when querying api in redux action

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);
});
};

Categories