Force component to wait for redux state update - reactjs

I know that question was asked a number of times, but none of answers work for me. I got the component, which job is to display user data from react-redux BUT userData can be stored in localStorage and on first load of website this localStorage is fetched to redux state, so I get the error where I want to display initials of currently logged in user. After adding async/await to mapStateToProps function localStorage gets fetched, but I still got error saying 'Cannot get 0 of undefined', using useEffect with pre set initials also gives an error. Here is my current code:
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { ReactComponent as Person } from 'assets/person.svg'
import { ReactComponent as Add } from 'assets/add.svg'
import { ReactComponent as Settings } from 'assets/settings.svg'
import { Styled, Container, Button } from './styles'
const UserInfo = ({ initial }) => {
let initials = 'UN'
useEffect(() => {
initials = initial
}, [initial])
return (
<Styled>
{initials}
<Container>
<Button>
<Add />
</Button>
<Button>
<Person />
</Button>
<Button>
<Settings />
</Button>
</Container>
</Styled>
)
}
const mapStateToProps = async ({ user }) => {
const initial = await `${user.name[0]}${user.surname[0]}`
return initial
}
export default connect(mapStateToProps)(UserInfo)
UserInfo.propTypes = {
initial: PropTypes.string.isRequired,
}
I also tried returning initial: initial || 'UNDEF' but I still receive an error. Additionally, when I don't refresh the browser on VSC save, I don't get an error. How can I force my component to wait for props from react-redux

I did it a little around, but now it works, i used useState and useEffect hooks and changed a bit mapStateToProps, that's my solution:
const [initials, setInitials] = useState('')
useEffect(() => {
if(name && surname) {
setInitials(`${name[0]}${surname[0]}`)
}
}, [name, surname])
and simple mapStateToProps
const mapStateToProps = ({ user }) => ({
name: user.name,
surname: user.surname,
})

This is not something that can be awaited: await '${user.name[0]}${user.surname[0]}' which is why you're still getting an error. You shouldn't need (or want, see https://react-redux.js.org/using-react-redux/connect-mapstate#mapstatetoprops-functions-should-be-pure-and-synchronous) to make mapStateToProps async. You're likely already doing the async call with an async redux action. You should either check for the presence of user name/surname and return a default state if undefined
const initial = user.name && user.surname ? '${user.name[0]}${user.surname[0]}' : 'UNDEF';
or add an isUserLoaded flag for the user state and return the default state, loading indicator, or not render that portion of UI until that flag is true.

Related

Why even after fetching the data and logging it, whenever I render it to the page after reloading the application breaks in React?

I am undertaking one of the projects from frontendmentor.io
Whenever I click any of the mapped data, with the help of react-router-dom I am redirecting it to url/countryName.
In this scenario, I am using react-router-dom to fetch a particular country while using the useLocation hook to fetch the url, the problem is everything works fine in the first render but as soon as I reload the website the application breaks. No matter how many times I try it never renders again.
import axios from 'axios'
import React,{ useEffect,useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
const Country = () => {
const location = useLocation()
const name = location.pathname.split('/')[1]
const navigate = useNavigate()
const [country,setCountry] = useState({})
useEffect(() => {
const getCountry = async() => {
try {
const res = await axios.get(`https://restcountries.com/v3.1/name/${name.toLowerCase()}?fullText=true`)
console.log(res.data,res)
setCountry(res.data)
} catch (error) {
console.log(error)
}
}
getCountry()
}, [name])
console.log(country[0].name.common) // returns undefined after reload
const backButton = (event) => {
event.preventDefault()
navigate('/')
}
return (
<div className='country-page page'>
<button onClick={backButton} className='backButton'>back button</button>
<div className='country-page-layout'>
<h2></h2>
</div>
</div>
)
}
export default Country
Other resources:
error message screenshot
API which I am using : https://restcountries.com/
You need to wait for your data to be fetched first, and then render it.
const [country,setCountry] = useState({})
//country is an empty object
//but here, your are trying to get the value from the array. (But country is an empty object)
console.log(country[0].name.common) // returns undefined after reload
The solution is to check if your data is here first
if (country?.[0]?.name?.common){
console.log(country[0].name.common)
}

Getting React to only load a component after an action has dispatched and completed

I have a component 'DashboardServiceEntry' that is used as an entry point to Dashboard.
'Dashboard' reads from Redux the current Options.viewLevel and uses this to make an API call and to show what to display.
Dispatching the action in useEffect in 'DashboardServiceEntry', updates the redux Store, but before this is available in the store The 'Dashboard' component is rendered and reads the wrong value in the redux store, triggering an unnecessary api call.
When the redux Store is updated correctly, another API call (the correct one is started).
How can I make it that the Dashboard component is loaded only after the store has been updated?
Thank you kind people.
import Dashboard from './Dashboard';
import { updateDashOptions } from '../../actions';
import { useDispatch } from 'react-redux';
export default function DashboardServiceEntry() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(
updateDashOptions({
viewLevel: 1,
viewLevel1Title: `Service List`,
})
);
}, [dispatch]);
return <Dashboard></Dashboard>;
}
function Dashboard() {
let options = useSelector((state) => state.options);
useEffect(() => {
const loadData = () => {
if (options.viewLevel === 0) {
let uri = createURI(options, options.view);
dispatch(fetchData(uri, 'SERVICES_SL', dispatch));
}
if (options.viewLevel === 1) {
let uri = createURI(options, 'service');
dispatch(fetchData(uri, 'SERVICES_SLSSD', dispatch));
}
};
loadData();
}, [dispatch, options]);
To avoid unnecessary api call, you can dispatch another action like isLoading:true and update it to false when you update the correct value of the options and only when its false, render the Dashboard component.

UseEffect doesn't behave as expected -

When I refresh the page useEffect only render one function out of the two inside, and when I change routes it works normally.
I am using redux to set global state with the vehicles and dispatching the API in useEffect
so I have vehicles available all the time. however, the second function vchStatusNumbers that it should return the length of the array filtered as per its status , it only runs once, and when i add its state as dependency i get an infinite loop!
I need to understand how i should approach it?
Below is the component
import React,{useEffect,useState} from "react";
import StatisticBanner from "./StatisticBanner";
import {getAllVehicles,fetchVehiclesReport } from "./vehiclesReducer";
import { useSelector, useDispatch } from "react-redux";
const Home= ()=> {
const {vehicles} = useSelector(getAllVehicles); // get the state
const [statusTotal, setStatusTotal] = useState({})
const dispatch = useDispatch(); // dispatch fn to reducers
useEffect(() => {
dispatch(fetchVehiclesReport());
vchStatusNumbers();
}, [dispatch]);
const vchStatusNumbers = () =>{
const status = {}
let availableLength = 0
let parkedLength = 0
let serviceLength = 0
vehicles.map(vch=>{
if(vch.status === 'available'){
++availableLength
status.available = availableLength
}
if(vch.status === 'parked'){
++parkedLength
status.parked = parkedLength
}
if(vch.status === 'service'){
++serviceLength
status.service = serviceLength
}
})
setStatusTotal (status)
}
return (
<>
<div style={{ margin: 20 }}>
<StatisticBanner key={"statics"} statusTotal={statusTotal} />
</div>
</>
);
}
export default Home
Yes, if you add a dependency to an useEffect hook that ultimately updates that dependency value then this will cause render looping.
Seems vchStatusNumbers should be in a separate useEffect hook with a dependency on the vehicles redux state value.
useEffect(() => {
vchStatusNumbers();
}, [vehicles]);
This is because it is derived state from the vehicles data and won't be updated yet in the first effect that dispatches the action to update it.

Component not re rendering when value from useContext is updated

I'm using React's context api to store an array of items. There is a component that has access to this array via useContext() and displays the length of the array. There is another component with access to the function to update this array via useContext as well. When an item is added to the array, the component does not re-render to reflect the new length of the array. When I navigate to another page in the app, the component re-renders and reflects the current length of the array. I need the component to re-render whenever the array in context changes.
I have tried using Context.Consumer instead of useContext but it still wouldn't re-render when the array was changed.
//orderContext.js//
import React, { createContext, useState } from "react"
const OrderContext = createContext({
addToOrder: () => {},
products: [],
})
const OrderProvider = ({ children }) => {
const [products, setProducts] = useState([])
const addToOrder = (idToAdd, quantityToAdd = 1) => {
let newProducts = products
newProducts[newProducts.length] = {
id: idToAdd,
quantity: quantityToAdd,
}
setProducts(newProducts)
}
return (
<OrderContext.Provider
value={{
addToOrder,
products,
}}
>
{children}
</OrderContext.Provider>
)
}
export default OrderContext
export { OrderProvider }
//addToCartButton.js//
import React, { useContext } from "react"
import OrderContext from "../../../context/orderContext"
export default ({ price, productId }) => {
const { addToOrder } = useContext(OrderContext)
return (
<button onClick={() => addToOrder(productId, 1)}>
<span>${price}</span>
</button>
)
}
//cart.js//
import React, { useContext, useState, useEffect } from "react"
import OrderContext from "../../context/orderContext"
export default () => {
const { products } = useContext(OrderContext)
return <span>{products.length}</span>
}
//gatsby-browser.js//
import React from "react"
import { OrderProvider } from "./src/context/orderContext"
export const wrapRootElement = ({ element }) => (
<OrderProvider>{element}</OrderProvider>
)
I would expect that the cart component would display the new length of the array when the array is updated, but instead it remains the same until the component is re-rendered when I navigate to another page. I need it to re-render every time the array in context is updated.
The issue is likely that you're mutating the array (rather than setting a new array) so React sees the array as the same using shallow equality.
Changing your addOrder method to assign a new array should fix this issue:
const addToOrder = (idToAdd, quantityToAdd = 1) =>
setProducts([
...products,
{
id: idToAdd,
quantity: quantityToAdd
}
]);
As #skovy said, there are more elegant solutions based on his answer if you want to change the original array.
setProducts(prevState => {
prevState[0].id = newId
return [...prevState]
})
#skovy's description helped me understand why my component was not re-rendering.
In my case I had a provider that held a large dictionary and everytime I updated that dictionary no re-renders would happen.
ex:
const [var, setVar] = useState({some large dictionary});
...mutate same dictionary
setVar(var) //this would not cause re-render
setVar({...var}) // this caused a re-render because it is a new object
I would be weary about doing this on large applications because the re-renders will cause major performance issues. in my case it is a small two page applet so some wasteful re-renders are ok for me.
Make sure to do the object or array to change every property.
In my examples, I updated the context from another file, and I got that context from another file also.
See my 'saveUserInfo' method, that is heart of the whole logic.

Redux: How to set default state of errorCode inside Redux store after clicking button wrapped in <Link> tag

I am trying to handle API error using Redux dispatch, by creating action, reducer & connect(mapStateToProps) at the end.
So when I enter wrong url, I get error message on my view.
But my button links wrapped inside tag are clicked then my next view shows a same the error message instead on rendering the new view.
// Actions
export const API_ERROR = "API_ERROR"
export const resetErrorCode = () => ({
type: RESET_ERROR_CODE
})
// Reducers
import { API_ERROR, RESET_ERROR_CODE } from "actions"
const api = (state = { errorCode: null }, action) => {
switch (action.type) {
case API_ERROR:
return { ...state, errorCode: action.errorCode }
case RESET_ERROR_CODE:
return { ...state, errorCode: action.errorCode }
default:
return { errorCode: null }
}
}
export default api
// Combiner reducers
import { combineReducers } from 'redux'
import API from "./api"
export default combineReducers({
API
});
// test.jsx
import { connect } from 'react-redux'
import API from 'api.js'
import { resetErrorCode } from 'actions'
import { Link } from 'react-router-dom'
class Test extends Component {
componentDidMount() {
API.call()
}
render() {
switch (this.props.errorCode) {
case 400:
case 404:
return (
<div>
Error Page
</div>
)
default:
return (
<div>
<Link to={{pathname: '/nextPage'}}> <button> Next Page </button> </Link>
</div>
)
}
}
componentWillUnmount() {
this.props.errorCode()
}
}
const mapStateToProps = (state) => {
return {
errorCode: state.apiStatus.errorCode,
}
}
const mapDispatchToProps = (dispatch) => {
return {
errorCode: () => dispatch(resetErrorCode())
}
}
export default connect(mapStateToProps)(Test)
// api.js
class API () {
responseErrorHandler(statusCode) {
console.log('responseErrorHandler() called')
store.dispatch(notifyAPIError(statusCode))
}
call = () => {
axios.get(url)
.then(...)
.catch((error) => this.responseErrorHandler(error.response.status))
}
}
I monitored my redux state while correct url redux state shows {errorCode: null} and when I enter wrong url redux state changes to {erroCode: 404}. Now If click my button, new view is rendered but the error code dont changes. Redux state is still {erroCode: 404}.
I tried to use componentWillUnmount to change the status of errorCode to null but it's not working.
Is there any solution to this problem and I'm not sure about my approach of componentWillUnmount().
I would appreciate the help.
I don't see much value on having the error on the Redux store if you're using it only inside a single component, and clearing it on unmount: a simple state would make things easier on this case.
If you're sure you want to go on that route, the right moment to clear the previous error is just when a new request is made, which happens on mounting of the component. This means that call() should reset the error before making the new call (which is now "loading"), and only set it again after it ends in an error.
Clearing at unmount would probably not be needed, as only pages which make the call need to check if an error happened, and those will reset it on mount.
Finally, some code bugs you might have:
Make sure your reducer is right: you don't usually modify the state on the "default" of the switch, as any other actions from other components would cause the state to get lost.
You're using the same prop name for the mapStateToProps and mapDispatchToProps, which is a problem. It doesn't fail because you're not using the mapDispatchToProps function on connect, so you might want to remove it entirely.

Resources