How to update useReducer states in React? - reactjs

I am trying to get the item from localStorage and pass it too useReducer State.
When i switch from one button to another button states is not updating. My initialState did update but states does not update
my initialStates and states on console.log both the values differ. states always stores the previous values of selected button Genid item value and initialStates stores the current selected values of selected button Genid item value but the main problem is did not update to useReducer states part
const Played = ( {Genid} ) => {
console.log("Played page ")
console.log("*************************************************")
console.log(Genid)
console.log(JSON.parse(localStorage.getItem(Genid)))
const initialState = {
DATA: JSON.parse(localStorage.getItem(Genid)) || [],
isFetching: localStorage.getItem(Genid) ? true : false,
hasError: false,
}
console.log(initialState);
const [states, dispatch] = useReducer(reducer, initialState)
console.log(states)

So the issue in your code was simple, the initialState wasn't populated in reducer because the displayid was undefined initially and then was set to ditto
The reason there is this behavior is because in Veritcal component you setDisplayId and that effect run ater initial render and by that time the undefined displayId is already being used by the Horizontal component
Now, there were other issues with your code too. You were updating sessionStorage with response.data.payload but there was no key as payload in response.data
Also Movement component is mapping on state.data but that is an object. Instead you need to map on state.data.abilities
Changed code snippets below
Movement.js
const Movement = ({ states }) => {
console.log(states);
return (
<div>
{states.data &&
states.data.abilities.map(ability => <div>{ability.ability.name}</div>)}
</div>
);
};
export default Movement;
Horizontal.js
console.log(displayid);
const initialState = {
// data: displayid? JSON.parse(sessionStorage.getItem(displayid)): [],
// isFetching: displayid && sessionStorage.getItem(displayid) ? true : false,
// hasError: false
};
const [states, dispatch] = useReducer(reducer, initialState);
// console.log(states);
useEffect(() => {
if (displayid && !sessionStorage.getItem(displayid)) {
console.log(displayid);
axios
.get(`https://pokeapi.co/api/v2/pokemon/${displayid}/`)
.then(response => {
console.log(response);
console.log(response.data);
dispatch({
type: "First",
payload: response.data
});
console.log(response.data);
sessionStorage.setItem(displayid, JSON.stringify(response.data));
})
.catch(error => {
console.log(error);
dispatch({
type: "Second"
});
});
} else if (displayid && sessionStorage.getItem(displayid)) {
// populate data from localStorage
const localData = {
data: JSON.parse(sessionStorage.getItem(displayid)) || [],
isFetching: sessionStorage.getItem(displayid) ? true : false,
hasError: false
};
dispatch({ type: "POPULATE", payload: localData });
}
}, [displayid]);
Working demo

Related

Redux state showing previous or default value while submitting function called

Async await is not functioning properly on my React project. It is not awaiting a response. Redux state displaying the previous or default value while calling the function but outside the function, it's working fine. As you can see, I print the employee from the selector in the handleSubmit function, however this prints the prior or default state, and I need the updated value to proceed. Data is not being awaited.
// handle submit function
const handleSubmit = async(values) => {
const personalData = new FormData();
Object.keys(personalValue).forEach((key) => personalData.append(key, personalValue[key]));
await dispatch(addEmployeePersonal(personalData));
console.log(employee) // Inside the function Employee prints default or previous state value
};
console.log(employee) // Here it's working fine, Outside the function employee prints updated value
// Selector
const {
employee,
employeeLoading,
employeeError
} = useSelector((state) => state.employee);
// Redux Reducer
export const employeeReducer = (state = {
employee: 0
}, action) => {
switch (action.type) {
case UPDATE_EMPLOYEE_PERSONAL_REQUEST:
case EMPLOYEE_ID_REQUEST:
case NEW_EMPLOYEE_PERSONAL_REQUEST:
case NEW_EMPLOYEE_OFFICIAL_REQUEST:
case DELETE_EMPLOYEE_REQUEST:
return {
...state,
employeeLoading: true,
employee: 0,
};
case UPDATE_EMPLOYEE_PERSONAL_SUCCESS:
case UPDATE_EMPLOYEE_OFFICIAL_SUCCESS:
case EMPLOYEE_ID_SUCCESS:
case NEW_EMPLOYEE_PERSONAL_SUCCESS:
case NEW_EMPLOYEE_OFFICIAL_SUCCESS:
case DELETE_EMPLOYEE_SUCCESS:
return {
...state,
employeeLoading: false,
employee: action.payload,
};
case UPDATE_EMPLOYEE_PERSONAL_FAILED:
case UPDATE_EMPLOYEE_OFFICIAL_FAILED:
case EMPLOYEE_ID_FAILED:
case NEW_EMPLOYEE_PERSONAL_FAILED:
case NEW_EMPLOYEE_OFFICIAL_FAILED:
case DELETE_EMPLOYEE_FAILED:
return {
...state,
employeeLoading: false,
employeeError: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
employeeError: null,
};
default:
return state;
}
};
// Redux Action
export const addEmployeePersonal = (info) => async(dispatch) => {
try {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_REQUEST
});
const {
data
} = await coreAxios.post("/api/Employee/PersonalInfo", info);
dispatch({
type: NEW_EMPLOYEE_PERSONAL_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_FAILED,
payload: error.response,
});
}
};
Reason: At the first render of the component, the value employee is 0. You trigger the form submit handler, it uses the employee, which value is 0. Even though you dispatch an action and try to change it, the handleSubmit still uses the employee evaluated in the first render(execution of function component) of the component before finishing executing. await dispatch(thunk()) will NOT wait for next render of the component. That's why you get the previous value of employee.
After handleSubmit finishes its execution, the state in the redux store has been changed, the redux store context provider will subscribe to that change and rerender the children. Your component will re-render(the second execution of the function component), useSelector will execute again and return the new employee. A new handleSubmit function will be declared and it reference the new employee defined in the function component scope.
There are two solutions:
Option 1: useEffect
useEffect(() => {
// get the latest employee to do something.
}, [employee])
Option 2: return action payload in thunk so that you can get it after dispatching action
export const addEmployeePersonal = (info) => async(dispatch) => {
try {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_REQUEST
});
const {
data
} = await coreAxios.post("/api/Employee/PersonalInfo", info);
dispatch({
type: NEW_EMPLOYEE_PERSONAL_SUCCESS,
payload: data,
});
return data; // here
} catch (error) {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_FAILED,
payload: error.response,
});
}
};
const handleSubmit = async(values) => {
const personalData = new FormData();
Object.keys(personalValue).forEach((key) => personalData.append(key, personalValue[key]));
const newEmployee = await dispatch(addEmployeePersonal(personalData));
// Do something with newEmployee.
};

Fetch not working after switch from class to functional component

I'm converting some class components to functional.
Console.log(data) returns the expected output but then once i try to set it using useState and check the value, it returns an empty array.
On the class component its working by using the state.
Functional Component ( Not Working )
const [submitting, setSubmitting] = useState(false)
const [watingForResult, setWaitingForResult] = useState(false)
const [submission, setSubmission] = useState([]);
const [scoringResults, setScoringResults] = useState([]);
+
function submitSubmission() {
setSubmitting(true);
setResult([]);
setError('');
let data = {
code: btoa(code),
language: { name: language.name },
users: { username: parseLocalJwt().username },
problem: { name: textToLowerCaseNoSpaces(problem.name) }
}
fetch(URL + '/submission', {
method: 'POST',
body: JSON.stringify(data),
headers: new Headers({
...getAuthorization(),
'Content-Type': 'application/json'
})
}).then(res => res.json()).then(data => {
setSubmitting(true);
setWaitingForResult(true);
setSubmission(data);
console.log('submitSubmission' + JSON.stringify(data));
window.secondsWaiting = new Date().getTime();
window.resultsListener = setInterval(fetchForResults(data.id), 1000);
});
}
...
function fetchForResults() {
console.log('data on fetchForResults' + submission.id)
}
...
<button
type="button"
onClick={submitSubmission}>
Submit!
</button>
console.log screenshot
Class component ( Working )
super(props);
this.state = {
problem: [],
sentSubmission: {
submitting: false,
waitingForResults: false,
submission: [],
scoringResults: []
},
results: {
loaded: false,
result: [],
error: ''
},
language: { mode: 'java', name: 'Java' },
code: ``
}
submitSubmission() {
this.setState({ sentSubmission: { submitting: true }, results: { result: [], error: '' } })
let data = {
code: btoa(this.state.code),
language: { name: this.state.language.name },
users: { username: parseLocalJwt().username },
problem: { name: textToLowerCaseNoSpaces(this.state.problem.name) }
}
fetch(URL + '/submission', {
method: 'POST',
body: JSON.stringify(data),
headers: new Headers({
...getAuthorization(),
'Content-Type': 'application/json'
})
}).then(res => res.json()).then(data => {
this.setState({ sentSubmission: { submitting: true, waitingForResults: true, submission: data } })
window.secondsWaiting = new Date().getTime();
window.resultsListener = setInterval(this.fetchForResults, 1000);
});
}
Some portion of the code is missing but, since you switched from a class component's state to a functional component state, I guess the issue is related to how you are using your state.
In functional components, when you set your state, you are changing the whole object in your state. So, when in your code you do setSentSubmission({ submitting: true });, your state becomes:
previousState = {
submitting: false,
waitingForResults: false,
submission: [],
scoringResults: []
}
nextState = {
submitting: true
//you lost watingForResults, submissions and scoring results
}
When using useState it is always suggested to decompose your object in different states:
const [submitting, setSubmitting] = useState(false)
const [watingForResult, setWaitingForResult] = useState(false)
//and so on...
//And then update them singularly:
//This...
setSentSubmission({ submitting: true });
//... become this
setSubmitting(true);
Finally, side note on lists. If you need to add a single element to a list in your state you can do:
setListState(currentList => [...currentList, newElement])
If this approach doesn't fit your use case and you need a more complex state management system I suggest you to look at useReducer (link).
Update
To print the content of your fetch you have either 2 choices.
Print directly the content retrieved in your fetch function
fetch('http://myendpoint.com/mydata.json')
.then(response => response.json())
.then(data => console.log(data));
If you need to do some processing to your data and you want to print what is the content of your updated state you can just use a useEffect hook. For instance, if you want to print the content of your scoringResult state you just write:
useEffect(() => {
console.log(scoringResult)
},[scoringResult])
This hook will be triggered every time scoringResult is updated (plus once when the component was mounted in the beginning)
Problem with your code is this:
setSentSubmission(...)
console.log(data);
console.log(sentSubmission.submission)
You can not set state and then to expect immediately to access that new state, new state can be accessed after component rerenders(in next iteration), all this is because you are reading sentSubmission.submission from closure which will be recreated only when component rerenders. So everything works fine with you code, you are just doing logging in a wrong place, move that log outside of the submitSubmission and you will see that state is updated successfully and that log will be printed after component rerenders(state updates).
When function submit() call, you set new value for sentSubmission.
Try this instead:
setSentSubmission((prevState) => (…prevState, { submitting: true }));

Struggling to Get Array Data From Redux Store

I have not been able to access items in an array that I am retrieving with redux. When I do console logs in the action itself, I am able to access array elements individually. But once that data makes its way to the component for display following the dispatch of actions, I have been unable to parse the data structure without error.
ListingActions.js:
If I do a console log here, I can parse through the different indices of the variable data without issue
export const getListings = () => async (dispatch) => {
try {
dispatch({ type: LISTING_REQUEST })
const { data } = await axios.get('/gmk')
// I can access the elements of the array here without a problem
dispatch({ type: LISTING_SUCCESS, payload: data })
} catch (error) {
dispatch({
type: LISTING_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
})
}
ListingReducers.js:
export const listingReducer = (state = { itemListings: [] }, action) => {
switch (action.type) {
case LISTING_REQUEST:
return { loading: true, itemListings: [] }
case LISTING_SUCCESS:
return { loading: false, itemListings: action.payload }
case LISTING_FAIL:
return { loading: false, error: action.payload }
default:
return state
}
Snippet from store.js:
const initialState = {
itemListings: [],
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
HomeScreen.js:
function HomeScreen() {
const dispatch = useDispatch()
const freshItemListings = useSelector((state) => state.itemListings)
const { loading, error, itemListings } = freshItemListings
useEffect(() => {
dispatch(getListings())
}, [dispatch])
return <div>{loading ? <p>Loading</p> : error ? <p>{error}</p> : itemListings.length}</div>
You'll see that I am trying to just access the length of itemListings. When I do so, I get an error: TypeError: Cannot read properties of undefined (reading 'length'). I have done other things like itemListings[0], and other methods just to see what my options are, but each has resulted in an error.
I believe this has to do with how you are updating itemListings in ListingReducers.js
case LISTING_SUCCESS:
return { loading: false, itemListings: action.payload }
You must update itemListing using the spread (...) operator to preserve the current state described here.
case LISTING_SUCCESS:
return {
...state,
loading: false,
itemListings: action.payload
}
In your code, the initial state is never actually getting updated in your reducer.
See Redux Documentation
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
WARNING
In Redux, our reducers are never allowed to mutate the original / current state values!
// ❌ Illegal - by default, this will mutate the state!
state.value = 123
TIP
Reducers can only make copies of the original values, and then they can mutate the copies.
// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}
Currently you have two itemListings in your app, which i think is causing some confusion.
In here :
const initialState = {
itemListings: [],
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
the default itemListings state from the store is an array.
Then in here:
const freshItemListings = useSelector((state) => state.itemListings)
freshItemListings is the same array and you are receiving it in the component
then in this line:
const { loading, error, itemListings } = freshItemListings
you are extracting a itemListings property from an array, which results to an error.
You have to define itemListings in the initial state as an object:
const initialState = {
itemListings: {itemListings:[]},
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
Another solution is as you are already defining a initial state for itemListings in its reducer, omit the itemListings property in initialState object,
const initialState = {
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
I discovered that the issue was with properly setting up the initial state in my store. I did change some of the variable names as suggested to make readability easier as well.
Changes to store.js:
const initialState = {
listingData: { itemListings: [], loading: true },
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
Setting both the loading and itemListing portions to the proper initial state was ultimately the answer.

Reducer not updating props in component correctly

My comments are dissappearing from my component after didMount() initializes them? It's really strange!
React component:
componentDidMount = (post) => {
const postId = this.props.post.id
console.log('postpreview', postId)
this.props.fetchComments(postId)
console.log('postpreview comments:', this.props.comments)
}
Redux Actions:
export const beginFetchComments = () => ({
type: C.BEGIN_FETCH_COMMENTS,
})
export const fetchCommentsFailed = (error) => ({
type: C.FETCH_COMMENTS_FAILED,
payload: { error },
})
export const fetchCommentsSuccess = (comments) => ({
type: C.FETCH_COMMENTS_SUCCESS,
payload: { comments }
})
export function fetchComments(postId) {
return dispatch => {
dispatch(beginFetchComments());
return fetch(`${api}/posts/${postId}/comments`, { headers })
.then(
res => res.json(),
error => console.log('An error occurred at fetchComments', error)
)
.then(json => {
dispatch(fetchCommentsSuccess(json));
return json;
});
};
}
Redux Reducer (switch case):
case C.BEGIN_FETCH_COMMENTS:
return {
...state,
loading: true,
error: null
};
case C.FETCH_COMMENTS_SUCCESS:
console.log(action.payload.comments);
const comments = _.mapKeys(action.payload.comments)
return {
...state,
loading: false,
comments,
};
The console displays this for the same console.log(), (I can't get my hands on my props!):
(2) [{…}, {…}]0: {id: "894tuq4ut84ut8v4t8wun89g", parentId: "8xf0y6ziyjabvozdd253nd", timestamp: 1468166872634, body: "Hi there! I am a COMMENT.", author: "thingtwo", …}1: {id: "8tu4bsun805n8un48ve89", parentId: "8xf0y6ziyjabvozdd253nd", timestamp: 1469479767190, body: "Comments. Are. Cool.", author: "thingone", …}length: 2__proto__: Array(0)
commentsReducer.js:22 []
I don't know what is the use of mapKeys here but what I would do is do a console.log to see if I'm getting an object and under what key there is a comments array:
case C.FETCH_COMMENTS_SUCCESS:
console.log(action.payload.comments); // is this logging an array?
return {
...state,
loading: false,
comments: action.payload.comments,
};
The bottom code I posted is the console.log - the object appears populated and then rerenders empty

Redux - why loading everything in state at root

I am trying to understand Redux and having some difficulty.
I understand the concept of combineReducer, ie ....
var reducer = combineReducers({
user: userReducer,
products: productsReducer
})
But what if I have thousands of products, only available on the products page. I do not understand why I need to load them at root; to me this will slow the initial start up of the app for something that will not be needed unless the user goes to the products page.
Is this just the way it is with redux?
In Redux apps, you always build your entire state at the start. With Redux you have one store and one state - everything should trickle down from that one state to props on your components. However, that does not mean you actually need to load all the data into the state at launch, only that the structure needs to be there. This is why you should set up an initial state object for each reducer.
Let's say you have thousands of product records that you load from the database. In your products reducer you could do something like this:
const initialState = {
data: []
};
//use ES6 default parameters
function productsReducer (state = initialState, action) {
switch (action.type) {
case 'GET_PRODUCTS':
//return data from action
return {
data: action.result
};
default:
return state;
}
}
This means that when you start your app, if you use the full reducer you declared in your post, your application state will look like this:
{
user: {},
products: {
data: []
}
}
products.data will be an empty array until you fire an action that actually requires you to load the products data (i.e. you go to the Products page in your app or something). It's true that the products data will remain in your state if you then go elsewhere in your app, but this is a great thing - the next time you render the Products page you will already have the data at your disposal without having to do a database lookup.
In our app, we made an API for the products and it has limit of 15 per page. So our reducer goes like this.
collection: {
"total": 0,
"per_page": 0,
"current_page": 0,
"last_page": 0,
"from": 0,
"to": 0,
data: []
},
isFetching: false,
isFetchingError: false
on the first load we fetched limited amount of products, then we made a pagination out of it.. using selectors in redux https://github.com/rackt/reselect
Loading a thousands of data will get your app very slow.
const paginated = (state = initialState, action) => {
switch (action.type) {
case FETCH_PAGINATED_PRODUCTS:
return {
...state,
isFetching: true,
isFetchingError: false
};
case FETCH_PAGINATED_PRODUCTS_SUCCESS:
return {
...state,
collection: action.payload,
isFetching: false
};
case FETCH_PAGINATED_PRODUCTS_ERROR:
return {
...state,
isFetching: false,
isFetchingError: true
};
default:
return state
we have used axios for request:
https://github.com/mzabriskie/axios
Here's how we implement axios in redux-async
export function getAll(page = 1) {
return (dispatch, getState) => {
const state = getState();
const { filters } = state.products.paginated;
if ( state.products.paginated.isFetching ) {
return;
}
dispatch({ type: FETCH_PAGINATED_PRODUCTS });
return axios
.get(`products?page=${page}&limit=16&filters=${JSON.stringify(filters)}`)
.then((res) => dispatch({
type: FETCH_PAGINATED_PRODUCTS_SUCCESS,
payload: res.data
}))
.catch((res) => dispatch({
type: FETCH_PAGINATED_PRODUCTS_ERROR,
/*payload: res.data.error,*/
error: true
}));
}
}
export function get(id) {
return (dispatch, getState) => {
const state = getState();
if ( state.products.resource.isFetching ) {
return;
}
dispatch({ type: FETCH_PRODUCT });
return axios
.get(`products/${id}`)
.then((res) => dispatch({
type: FETCH_PRODUCT_SUCCESS,
payload: res.data.data
}))
.catch((res) => dispatch({
type: FETCH_PRODUCT_ERROR,
/*payload: new Error(res.data.error),*/
error: true
}));
}

Resources