Updating react/redux state from inside a submit handler function - arrays

I'm currently working on a poll app which is supposed to sequentially render a list of questions and post answers to the server. I have no problem handling answers but looping through questions gave me some trouble.
Here is a flow of my code:
PollContainer.js - component
import React, { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Question from './Questions/Question';
import { pushAnswers } from '../../actions/answers';
import { incrementCounter } from '../../actions/utils';
import Thanks from './Thanks'
const PollContainer = () => {
const dispatch = useDispatch();
const questions = useSelector(state => state.questions); // an array of questions
// a counter redux state which should increment at every click of 'submit' inside a question
const utils = useSelector(state => state.utils);
let activeQuestion = questions[utils.counter];
// function passed as a prop to a singular Question component to handle submit of an answer
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers;
answersUpdate.push(answer);
console.log(`counter:${utils.counter}`) // logs 0 everytime I click submit, while redux dev tools show that utils.counter increments at every click
if (utils.counter < questions.length ) {
setState({...state, answers: answersUpdate, activeQuestion: activeQuestion});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({...state, isFinished: true});
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer
})
return (
(utils.isLoading) ? (
<h1>Loading questions...</h1>
) : (
<div>
{(!state.isFinished && <Question { ...state }/>)}
{(state.isFinished && <Thanks/>)}
</div>
))
}
export default PollContainer;
incrementCounter action:
import * as types from "./types";
export const incrementCounter = () => {
return {
type: types.INCREMENT_COUNTER,
}
}
utils.js - reducer
// reducer handles what to do on the news report (action)
import * as types from '../actions/types';
const initialState = {
isLoading: false,
error: null,
counter: 0
}
export default (utils = initialState, action) => {
switch(action.type){
case types.LOADING_DATA:
return {...utils, isLoading: true};
case types.DATA_LOADED:
return {...utils, isLoading: false};
case types.ACTION_FAILED:
return {...utils, error: action.error};
case types.INCREMENT_COUNTER:
return {...utils, counter: utils.counter + 1} // here is the incrementing part
default:
return utils;
}
}
utils.counter that is passed to pushSingleAnswer function doesn't increment, however redux dev tools tells me it does increase every time I click submit in a Question component. Because of that it doesn't render next questions. The submit handler in Question component is simply this:
const handleSubmit = (e) => {
e.preventDefault();
props.pushSingleAnswer(state);
};
I also tried with:
useEffect(() => {
dispatch(incrementCounter())},
[state.answers]
);
expecting it'll increment every time there's an update to state.answers but it doesn't work either. Morover the counter in redux-dev-tools doesn't increment either.
I'd be very grateful for any suggestions, this is my first serious react-redux project and I really enjoy working with these technologies. However I do not quite understand how react decides to render stuff on change of state.

Issue
You are closing over the initial counter state in the pushSingleAnswer callback stored in state and passed to Question component.
You are mutating your state object in the handler.
Code:
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers; // <-- save state reference
answersUpdate.push(answer); // <-- state mutation
console.log(`counter:${utils.counter}`) // <-- initial value closed in scope
if (utils.counter < questions.length ) {
setState({
...state, // <-- persists closed over callback/counter value
answers: answersUpdate,
activeQuestion: activeQuestion,
});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer // <-- closed over in initial state
});
{(!state.isFinished && <Question { ...state }/>)} // <-- stale state passed
Solution
Don't store the callback in state and use functional state updates.
const pushSingleAnswer = (answer) => {
console.log(`counter:${utils.counter}`) // <-- value from current render cycle
if (utils.counter < questions.length ) {
setState(prevState => ({
...prevState, // <-- copy previous state
answers: [
...prevState.answers, // <-- copy previous answers array
answer // <-- add new answer
],
activeQuestion,
}));
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
});
{!state.isFinished && (
<Question { ...state } pushSingleAnswer={pushSingleAnswer} />
)}

Related

How can I keep my useSelector() variable from a redux store updated after each dispatch?(Redux Toolkit)

So I'm using React Native with Redux Toolkit and I'm facing this issue where once I dispatch and decrement a store value, the variable inside the component that grabs the state from the store does not update until the next render. It would be nice to know what the current value is at all times. I was thinking something like useEffect(), but I'm not sure how I would set that up. Any ideas would be appreciated.
Here is the component where the dispatch happens(inside handlePress function) and also where the redux state variable "exercises" I grab is found.
const LoggingButton = ({ disabled, maxReps, setIndex, exerciseIndex }) => {
const dispatch = useDispatch()
if (!maxReps)
maxReps = 5
const [count, setCount] = useState(maxReps)
const [active, setActive] = useState(false)
const { exercises } = useSelector((state) => state.workout);
const handlePress = () => {
if (!active)
setActive(true)
else {
dispatch(decrement({exerciseIndex:exerciseIndex,setIndex:setIndex}))
}
console.log("pressed",exercises[exerciseIndex].sets)
}
if (disabled)
return (
<Button
disabled round style={styles.disabledState}
iconSource={() => (<FontAwesomeIcon icon={faTimes} color={'#282c34'} size={30} />)}
/>)
else
return (
<Button
label={(count != 0) ? count : '0'} round
style={(active) ? styles.loggingState : styles.initialState} labelStyle={(active) ? styles.loggingLabel : styles.initialLabel}
onPress={handlePress}
/>)
}
Here is the workout state if it helps:
const currentWorkoutSlice = createSlice({
name: "currentWorkout",
initialState: {
id: "A",
exercises: [
],
timeStarted: '',
},
reducers: {
setWorkout(state, action) {
state.id = action.payload
},
setCurrentExercises(state, action) {
state.exercises = action.payload
},
setTimeStarted(state, action) {
state.timeStarted = action.payload
},
decrement(state, action) {
let newList = state.exercises.map(e=>e)
const {exerciseIndex,setIndex} = action.payload
newList[exerciseIndex].sets[setIndex] -=1
}
}
})
The whole point of useSelector is to update your component as soon as that state changes so you always render the most up-to-date state contents.
"Until next render" is an incredibly short amount of time here and will often even happen synchonically. You should not really care about that.
Also, due to scoping those are two very different variables that just randomly have the same name - there is no way one could update the other.

How to properly compose functional components that have infinite scrolling logic

I'm converting a class component to functional component for practice. It has a ref object to contain some variables for the component, such as IntersectionObserver object to implement infinite scrolling.
The issue starts from here. The callback function of the IntersectionObserver calls a function(says update) defined in the component to load more data. Because the IntersectionObserver is defined inside the useRef, the update function is the function bound when the component gets initialized. So the value of the state that is used in the update function is also the value of the initial state.
How can I compose this functional component in a proper way?
Backbone demo
export default function A(props) {
const [state, setState] = useState({
pageNo: 1,
isLoading: false,
items: []
});
const update = useCallback(() => {
setState(state => ({...state, isLoading: true}));
someApi(state.pageNo);
setState(state => ({
...state,
pageNo: pageNo + 1
}));
setState(state => ({...state, isLoading: false}));
}, [isLoading, pageNo]);
const observerCallback = useCallback((entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
observer.disconnect();
update();
}
}
}, [update]);
const observer = useRef(new IntersectionObserver(observerCallback)); // The callback is the function binding the update function that binds some of the initial state
const lastEl = useRef(null);
const preLastEl = useRef(null);
useEffect(() => {
update();
}, [props]);
if (lastEl.current && lastEl.current != preLastEl.current) {
preLastEl.current = lastEl.current;
observer.observe(lastEl.current);
}
return (
<SomeProgressBar style={{ display: state.isLoading ? "" : "none" }}/>
{
state.items.map((item) => <B ... ref={lastEl}/>)
}
);
}
I don't exactly why you're using the ref and why you can't do it differently. so in case you have to do it this way, your refs are dependent to state object and they need to be changed when the state are changed so you should use a useEffect to change the refs based on new state. try to implement one of these two steps:
1
const refs = useRef({
lastEl: undefined,
observer: new IntersectionObserver((entries, observer) => {
...
update(state.pageNo); // This is the update function bound when the instance of this component gets initialized
});
});
useEffect(() => {
update(state.pageNo);
}, [props]);
function update(pageNo = 1) {
setState(prev => ({...prev, isLoading: true}));
someApi(pageNo); // state.pageNo will be always 1
setState(prev => ({...prev, isLoading: false}));
}
2 in case above code didn't work try this
useEffect(() => {
if(state.pageNo){
refs.current = {
lastEl: undefined,
observer: new IntersectionObserver((entries, observer) => {
...
update(state.pageNo); // This is the update function bound when the instance of this component gets initialized
});
}
}
}, [state.pageNo])

React hooks, declaring multiple state values from prop

I'm pretty new to React and React hooks in general,
I'm building a react app for my final project and I wanted to make some component (Advanced search in this example) as generalized as possible which means I want to pass "dataFields" and the component should be updated with a unique state value that originated from those data fields.
I know that I can use a general state and store changes in it with an array but I read that it's bad practice.
this is what I have now:
const [title,updateTitle] = useState({"enable":false,"value": "" });
const [tags,updateTags] = useState({"enable":false,"value": "" });
const [owner,updateOwner] = useState({"enable":false,"value": "" });
const [desc,updateDesc] = useState({"enable":false,"value": "" });
And I try to use this to achieve the same thing:
if(props?.dataFields) {
Object.entries(props.dataFields).forEach ( ([key,value]) => {
// declare state fields
const [key,value] = useState(value)
});
}
what is the proper way of doing it? is there is one?
Do 4 lines of useState or useReducer (local)
I would suggest someting like this for the initial state
const setItem = (enable = false, value = '') => ({ enable, value });
const [title, updateTitle] = useState(setItem());
const [tags, updateTags] = useState(setItem());
const [owner, updateOwner] = useState(setItem());
const [desc, updateDesc] = useState(setItem());
And you also can useReducer and define the initial state.
I add an example for useReducer and case dor change title.value
import React from 'react';
import { useReducer } from 'react';
const setItem = (enable = false, value = '') => ({ enable, value });
const initialState = { title: setItem(), tags: setItem(), owner: setItem(), desc: setItem() };
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_TITLE':
return { ...state, title: setItem(null, action.payload) };
default:
return state;
}
}
function MyFirstUseReducer() {
const [state, dispatch] = useReducer(reducer, initialState);
const updateTitle = ev => {
if (ev.which !== 13 || ev.target.value === '') return;
dispatch({ type: 'CHANGE_TITLE', payload: ev.target.value });
ev.target.value = '';
};
return (
<>
<h2>Using Reducer</h2>
<input type="text" onKeyUp={updateTitle} placeholder="Change Title" />
<div>
<span>The State Title is: <strong>{state.title.value}</strong></span>
</div>
</>
);
}
export default MyFirstUseReducer;

Second call to setState prevents first call to setState from executing

I am making a rock, paper, scissors game. In the code below, I have a context file that is used to store the global state. I also am showing my choices component. When a user clicks on a button in the choices component, the setChoices method is called which should set the user choice and cpu choice variables in the global state. Then, the cpuScore() method is ran afterwards to increment the cpu score (just to illustrate the problem). The cpu score updates as expected, but the choice variables are not updated. If I do not run the cpuScore method, the choice variables update as expected, but obviously not the score.
//context file
import React, { createContext, useState } from 'react';
const GameContext = createContext();
const GameContextProvider = props => {
const [gameItems, setGameItems] = useState(
{userChoice: null, userScore: 0, cpuChoice: null, cpuScore: 0}
);
const setChoices = (userChoice, cpuChoice) => {
setGameItems({...gameItems, userChoice: userChoice, cpuChoice: cpuChoice})
}
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
return (
<GameContext.Provider value={{gameItems, setChoices, cpuScore}}>
{ props.children }
</GameContext.Provider>
)
}
export default GameContextProvider;
//choices component
import React, { useContext } from 'react';
import { GameContext } from '../contexts/GameContext';
const Choices = (props) => {
const { setChoices, cpuScore } = useContext(GameContext);
const getCpuChoice = () => {
const cpuChoices = ['r', 'p', 's'];
const randomIndex = Math.floor((Math.random() * 3));
const cpuDecision = cpuChoices[randomIndex];
return cpuDecision
}
const playGame = (e) => {
const userChoice = e.target.id;
const cpuChoice = getCpuChoice();
setChoices(userChoice, cpuChoice);
cpuScore();
}
return (
<div>
<h1>Make Your Selection</h1>
<div className="choices">
<i className="choice fas fa-hand-paper fa-10x" id="p" onClick={playGame}></i>
<i className="choice fas fa-hand-rock fa-10x" id="r" onClick={playGame}></i>
<i className="choice fas fa-hand-scissors fa-10x" id='s' onClick={playGame}></i>
</div>
</div>
)
What do I need to change to set the state for both choice and score?
Since calls to setState are asynchronous, your two calls to setState are interfering with each other, the later one is overwriting the earlier one.
You have a few options.
Separate your state so that the values don't affect each other:
const [choices, setChoices] = useState({ user: null, cpu: null });
const [scores, setScores] = useState({ user: 0, cpu: 0);
Or go even further and set each of the two choices and two scores as their own state value.
Keep all state in one object, but update it all at once:
const setChoices = (userChoice, cpuChoice) => {
const cpuScore = gameItems.cpuScore + 1;
setGameItems({
...gameItems,
userChoice,
cpuChoice,
cpuScore
});
}
Use useReducer:
const initialState = {
userChoice: null,
userScore: 0,
cpuChoice: null,
cpuScore: 0
}
const [gameItems, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "UPDATE_CHOICES":
return {
...state,
userChoice: action.userChoice,
cpuChoice: action.cpuChoice
};
case 'UPDATE_CPU_SCORE':
return {
...state,
cpuScore: state.cpuScore + 1
}
default:
return state;
}
}, initialState);
const setChoices = (userChoice, cpuChoice) => {
dispatch({ type: 'UPDATE_CHOICES', userChoice, cpuChoice });
};
const cpuScore = () => {
dispatch({ type: 'UPDATE_CPU_SCORE'})
};
Basically, React doesn't immediately updates after you call setState.
If you call cpuScore() right after setChoices(), the cpuScore function you are calling is still the function from the current render, not after setChoices() update. Because of that, cpuScore() will set the state again, using the spread value of gameItems (which is still hasn't changed because the update from setChoices hasn't kicked in) which cause the changes by setChoices() to be overridden.
If React always immediately updates after every setState call, the performance would be atrocious. What React does is it will batch multiple setState calls into one update, so it doesn't update the DOM repeatedly.
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
My suggestion would be either to separate this two states, so they don't get overwritten by each other, or create a new function that handle all this updates in one place.

Implementing infinite scroll with React/Redux and react-waypoint issue

Im struggling to achieve infinite scroll with my test React/Redux application.
Here how it works in simple words:
1) On componentDidMount I dispatch an action which sets the Redux state after getting 100 photos from the API. So I got photos array in Redux state.
2) I implemented react-waypoint, so when you scroll to the bottom of those photos it fires a method which dispatches another action that get more photos and "appends" them to the photos array and...
as I understand - the state changed, so redux is firing the setState and the component redraws completely, so I need to start scrolling again but its 200 photos now. When I reach waypoint again everything happens again, component fully rerenders and I need to scroll from top through 300 photos now.
This is not how I wanted it to work of course.
The simple example on react-waypoint without Redux works like this:
1) you fetch first photos and set the components initial state
2) after you scroll to the waypoint it fires a method which makes another request to the api, constructs new photos array(appending newly fetched photos) and (!) call setState with the new photos array.
And it works. No full re-renders of the component. Scroll position stays the same, and the new items appear below waypoint.
So the question is — is the problem I experience the problem with Redux state management or am I implementing my redux reducers/actions not correctly or...???
Why is setting component state in React Waypoint Infinite Scroll example (no Redux) works the way I want (no redrawing the whole component)?
I appreciate any help! Thank you!
The reducers
import { combineReducers } from 'redux';
const data = (state = {}, action) => {
if (action.type === 'PHOTOS_FETCH_DATA_SUCCESS') {
const photos = state.photos ?
[...state.photos, ...action.data.photo] :
action.data.photo;
return {
photos,
numPages: action.data.pages,
loadedAt: (new Date()).toISOString(),
};
}
return state;
};
const photosHasErrored = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
};
const photosIsLoading = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_IS_LOADING':
return action.isLoading;
default:
return state;
}
};
const queryOptionsIntitial = {
taste: 0,
page: 1,
sortBy: 'interestingness-asc',
};
const queryOptions = (state = queryOptionsIntitial, action) => {
switch (action.type) {
case 'SET_TASTE':
return Object.assign({}, state, {
taste: action.taste,
});
case 'SET_SORTBY':
return Object.assign({}, state, {
sortBy: action.sortBy,
});
case 'SET_QUERY_OPTIONS':
return Object.assign({}, state, {
taste: action.taste,
page: action.page,
sortBy: action.sortBy,
});
default:
return state;
}
};
const reducers = combineReducers({
data,
photosHasErrored,
photosIsLoading,
queryOptions,
});
export default reducers;
Action creators
import tastes from '../tastes';
// Action creators
export const photosHasErrored = bool => ({
type: 'PHOTOS_HAS_ERRORED',
hasErrored: bool,
});
export const photosIsLoading = bool => ({
type: 'PHOTOS_IS_LOADING',
isLoading: bool,
});
export const photosFetchDataSuccess = data => ({
type: 'PHOTOS_FETCH_DATA_SUCCESS',
data,
});
export const setQueryOptions = (taste = 0, page, sortBy = 'interestingness-asc') => ({
type: 'SET_QUERY_OPTIONS',
taste,
page,
sortBy,
});
export const photosFetchData = (taste = 0, page = 1, sort = 'interestingness-asc', num = 500) => (dispatch) => {
dispatch(photosIsLoading(true));
dispatch(setQueryOptions(taste, page, sort));
const apiKey = '091af22a3063bac9bfd2e61147692ecd';
const url = `https://api.flickr.com/services/rest/?api_key=${apiKey}&method=flickr.photos.search&format=json&nojsoncallback=1&safe_search=1&content_type=1&per_page=${num}&page=${page}&sort=${sort}&text=${tastes[taste].keywords}`;
// console.log(url);
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(photosIsLoading(false));
return response;
})
.then(response => response.json())
.then((data) => {
// console.log('vvvvv', data.photos);
dispatch(photosFetchDataSuccess(data.photos));
})
.catch(() => dispatch(photosHasErrored(true)));
};
I also include my main component that renders the photos because I think maybe it's somehow connected with the fact that i "connect" this component to Redux store...
import React from 'react';
import injectSheet from 'react-jss';
import { connect } from 'react-redux';
import Waypoint from 'react-waypoint';
import Photo from '../Photo';
import { photosFetchData } from '../../actions';
import styles from './styles';
class Page extends React.Component {
loadMore = () => {
const { options, fetchData } = this.props;
fetchData(options.taste, options.page + 1, options.sortBy);
}
render() {
const { classes, isLoading, isErrored, data } = this.props;
const taste = 0;
const uniqueUsers = [];
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
return (
<div className={classes.wrap}>
<main className={classes.page}>
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
</main>
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}><Waypoint onEnter={() => this.loadMore()} /></div>}
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}>Loading...</div>}
</div>
);
}
}
const mapStateToProps = state => ({
data: state.data,
options: state.queryOptions,
hasErrored: state.photosHasErrored,
isLoading: state.photosIsLoading,
});
const mapDispatchToProps = dispatch => ({
fetchData: (taste, page, sort) => dispatch(photosFetchData(taste, page, sort)),
});
const withStore = connect(mapStateToProps, mapDispatchToProps)(Page);
export default injectSheet(styles)(withStore);
Answer to Eric Na
state.photos is an object and I just check if its present in the state. sorry, in my example I just tried to simplify things.
action.data.photo is an array for sure. Api names it so and I didn't think about renaming it.
I supplied some pics from react dev tools.
Here is my initial state after getting photos
Here is the changed state after getting new portion of photos
There were 496 photos in the initial state, and 996 after getting
additional photos for the first time after reaching waypoint
here is action.data
So all I want to say that the photos are fetched and appended but it triggers whole re-render of the component still...
I think I see the problem.
In your component you check for
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
once you make another api request, in your action creator you set isLoading to true. this tells react to remove the whole photos component and then once it's set to false again react will show the new photos.
you need to add a loader at the bottom and not to remove the whole photos component once fetching and then render it again.
EDIT2
Try commenting out the whole uniqueUsers part (let's worry about the uniqueness of the users later)
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
and instead of
photos.map(photo =>
(<Photo ..
try directly mapping data.photos?
data.photos.map(photo =>
(<Photo ..
EDIT
...action.data.photo] :
action.data.photo;
can you make sure it's action.data.photo, not action.data.photos, or even just action.data? Can you try logging the data to the console?
Also,
state.photos ? .. : ..
Here, state.photos will always evaluate to true-y value, even if it's an empty array. You can change it to
state.photos.length ? .. : ..
It's hard to tell without actually seeing how you update photos in reducers and actions, but I doubt that it's the problem with how Redux manages state.
When you get new photos from ajax request, the new photos coming in should be appended to the end of the photos array in the store.
For example, if currently photos: [<Photo Z>, <Photo F>, ...] in Redux store, and the new photos in action is photos: [<Photo D>, <Photo Q>, ...], the photos in store should be updated like this:
export default function myReducer(state = initialState, action) {
switch (action.type) {
case types.RECEIVE_PHOTOS:
return {
...state,
photos: [
...state.photos,
...action.photos,
],
};
...

Resources