Why does my state not appear to be updating in my component? - javascript

I am new to Redux, though I have done a bit of work with React before.
I'm using a tutorial to test using Actions, Action Creators, and Reducers in my application, and so far I think I'm about 90% of the way there.
componentDidMount() {
this.props.levelUp();
this.props.levelUp();
this.props.levelUp();
this.props.levelUp();
this.props.levelUp();
console.log("final prop");
console.log(this.props);
}
const mapStateToProps = (state) => {
console.log("state general");
console.log(state.general);
return {
general: state.general,
};
};
Both of the console logs get triggered here, and they increment with each level up operation or decrement with every level down operation.
function mapDispatchToProps(dispatch) {
return bindActionCreators(generalActions, dispatch);
}
This is in my reducer file:
export default (state = 1, action) => {
console.log(state);
switch (action.type) {
case 'LEVEL_UP':
console.log(action.type);
return state + 1;
case 'LEVEL_DOWN':
return state - 1;
}
return state;
};
My console logs here seem to be capturing the right increment - the value in the reducer goes up one every time I call this.props.levelUp()
However when I do the final logging of the props in componentDidMount(), the value is 1.
Why is this? Am I not persistently saving the data? Is there some other reason why I'm not returning state the way I am envisioning?

componentDidMount will be fired once the component did mount. Afterwards your actions are fired hence why you should do your console.log() statements inside of something like componentDidUpdate() or static getDerivedStateFromProps().
More about lifecycles in react: https://reactjs.org/docs/state-and-lifecycle.html
Greetings

Related

React/Redux component with checkboxes does not update when click on checkbox even though I'm returning new state

I've been stuck for a while trying to make the re-render in the checkboxes to work, the variables are being updated but it's just the rendering that doesn't happen.
I'm receiving a response from the backend that contains an object with an array of steps, I'm going to render a checkbox for every step if it's from a specific type. As soon as I received the object, I add in every step a new property value to use it later for checking the checkboxes.
This is my reducer:
export const MyObject = (state: MyObject = defaultState, action: FetchMyObjectAction | UpdateStepsInMyObjectAction) => {
switch (action.type) {
case "FETCH_MYOBJECT":
return {
...action.payload, // MyObject
steps: action.payload.steps.map((step) => {
if (step.control.controlType === "1") { // "1" = checkbox
return {
...step,
value: step.control.defaultValues[0] === "true" ? true : false, // Adding the property value
};
}
return step;
}),
};
case "UPDATE_STEPS":
return {
...state,
steps: state.steps.map((step) => {
if (step.id === action.payload.stepId) { // if this is the checkbox to update
return {
...step,
value: action.payload.checked,
};
}
return step;
}),
};
default:
return state;
}
This is how I'm rendering the checkboxes:
for (let step of steps) {
if (step.control.controlType === "1") {
controls.push(
<Checkbox
label={step.displayName}
checked={step.value}
onChange={(_ev, checked) => {
callback(step.id, checked);
}}
disabled={false}
className={classNames.checkbox}
/>
);
}
}
callback is a function that calls the reducer above for the case "UPDATE_STEPS".
After inspecting the variables I can see that they are being updated properly, it's just that the re-render doesn't happen in the checkboxes, not even the first time I check the box, the check doesn't appear. If I move to a different component and then go back to the component with the checkboxes I can see now the checks. But if I check/uncheck within the same component, nothing happens visually.
As far as I know, I'm returning new objects for every update, so mutability is not happening. Can you see what I'm missing?
Thanks!
First I would inspect if the checkbox works with useState to manage your state.
import { useState } from "react";
function CheckBoxForm() {
const [checked, setChecked] = useState(false);
return <Checkbox checked={checked} onChange={() => setChecked(!checked)} />;
}
Then I would check if you have wired up the reducer correctly using redux or useReducer. When you dispatch an action it should trigger a rerender. For troubleshooting this using redux please refer to the redux docs: https://react-redux.js.org/troubleshooting#my-views-aren-t-updating-when-something-changes-outside-of-redux.
You may be updating the object directly rather than dispatching an action using the function provided by the redux store. If you are using hooks, here is how you wire up your app to make sure the component props are subscribed to changing in the redux store. You must wrap with a provider, use redux hooks like useSelector and use their provided dispatch function: https://react-redux.js.org/api/hooks
Using useReducer is a much simpler process and will be easier to troubleshoot: https://beta.reactjs.org/reference/react/useReducer

Redux connected react app - how do I NOT render it until my side effect is complete?

React 16.3
Redux 3.7.2
In componentDidMount I call an action that populates the props (mapDispatchToProps) thru an side effect (AJAX).
My problem is that it reuses the old props from redux - render that first and then re-render once the update has arrived thru mapDispatchToProps.
I don't want to have this child component remember it's state or props - I need it to be a blank slate each time.
The documentation states that the the component is destroyed on unmount but that doesn't seem to be the case since it's a difference in state when you go to this page or you are on the page and reload.
Because when you reload there is no data so the page HAS to wait for the sideeffect to complete.
Unfortunately I can't provide you with a code sample - but anyone that has been exposed to this weird behavior should recognize my description....
Your component is likely re-rendering with the "old" state because that's the state you've stored in redux, and are passing to the component.
What you could do is trigger a redux action in componentWillUnmount which destroys that state.
EG (pseudo-pseudocode):
Reducer:
const initialState = { counter: 0 };
const myReducer = (state = initialState, action) => {
switch(action.type) {
case 'DO_SOMETHING':
return {
...state,
counter: state.counter + 1
};
case 'CLEAN_STATE':
return initialState;
}
}
Component:
class MyComponent extends React.Component {
render () {
return (
<div>
<span>{this.props.counter}</span>
<button onClick={triggerActionThatCausesDO_SOMETHING} />
</div>
);
}
componentWillUnmount () {
triggerActionThatCausesCLEAN_STATE();
}
}

Can not get new props after action dispatching

I have a React component:
class Board extends React.Component {
// ...
compareLastPair() {
const {gameplay} = this.props;
console.log('after dispatching, before compare', gameplay.clickedTiles); //-> [1]
// i got old state as before dispatching an action, but expected refreshed props
// thus, condition below never executes
if(gameplay.board.length === gameplay.pairedTiles.length + gameplay.clickedTiles.length) {
this.compareTiles();
}
}
handleClick(id) {
const {gameplay, dispatch} = this.props;
// ...some code
else if(!gameplay.clickedTiles.includes(id) && gameplay.clickedTiles.length < 2) {
console.log('before dispatching', gameplay.clickedTiles); // -> [1]
dispatch(clickTile(id));
this.compareLastPair();
}
}
//...
}
My reducer dispatches sync action:
const gameplay = (state = {board: [], clickedTiles: [], pairedTiles: [], round: 0}, action) => {
switch(action.type) {
case 'CLICK_TILE':
return {...state, ...{clickedTiles: state.clickedTiles.concat(action.id)}}
}
}
My question is: why my compareLastPair function gets the same props as before dispatching in handleClick function, despite the fact that the state was updated by Redux(you can see it in Redux-logger at the image) and clickedTiles array should be concantenated by reducer.
Even if your dispatch action is synchronous (but we don't know... you didn't shared the code), props update in the React component follow the normal asynchronous lifecycle, while you are explicitly calling compareLastPair after the dispatch.
React/Redux do not work this way: new props will be received by your component after your call.
For your test, I suggest you to call compareLastPair inside the componentDidUpdate lifecycle method, which is called after prop changes.

Adding/Removing animation classes with react + redux

I want to display quick flash animations on certain events (eg. a red border flash for each incorrect keystroke).
To do this with css animations, I need to remove and add the animation class each time I want to trigger the flash. (Unless there's another way to retrigger an animation?).
There are a few suggestions for doing this on this github thread: https://github.com/facebook/react/issues/7142
However, in my case the state that triggers the flash is the redux state. And in many cases the state hasn't actually changed, so it doesn't cause a rerender.
Here's the best solution I've got, which involves setting a random number to force a re-render. Is there a better way to do this?
reducer.js
//Reducer function to update redux state
function setError(state, action) {
state.hasError = true;
state.random = Math.random();
return state;
}
export default function allReducers(state = initialState, action) {
switch (action.type) {
case ActionTypes.SUBMIT_VALUE_BUTTON:
return Object.assign({}, state, setError(state, action));
default:
return state;
}
}
react component and container
const mapStateToProps = (state, ownProps) => {
return {
random: state.random,
hasError: state.hasError,
}
}
componentWillReceiveProps() {
this.setState({hasError: this.props.hasError});
setTimeout(() => {
this.setState({hasError: false});
}, 300)
}
render() {
return <div className = {`my-component ${this.state.hasError ? 'has-error':''}`} />;
}
Edit: It's worth noting that the redux documentation says that you shouldn't call non-pure functions like Math.random in a reducer method.
Things you should never do inside a reducer:
Call non-pure functions, e.g. Date.now() or Math.random().
Your code has a few problems in it, I'll go one by one...
You can't mutate the state object on the reducer. Here it is from the redux docs:
Note that:
We don't mutate the state. We create a copy with Object.assign().
Object.assign(state, { visibilityFilter: action.filter }) is also
wrong: it will mutate the first argument. You must supply an empty
object as the first parameter. You can also enable the object spread
operator proposal to write { ...state, ...newState } instead.
In your code setError receives the state as a prop and mutates it. setError should look like this:
function setError(state, action) {
let newState = Object.assign({}, state);
newState.hasError = true;
newState.random = Math.random();
return newState;
}
The second problem might be because there's some code missing but I cant see when your'e changing your state back to no errors so the props doesnt really change.
In your componentWillReceiveProps your referencing this.props instead of nextProps.
componentWillReceiveProps should look like this:
componentWillReceiveProps(nextProps) {
if (nextProps.hasError !== this.props.hasError && nextProps.hasError){
setTimeout(() => {
// Dispatch redux action to clear errors
}, 300)
}
}
And in your component you should check for props and not state as getting props should cause rerender (unless the render is stopped in componentShouldUpdate):
render() {
return <div className={`my-component ${this.props.hasError ? 'has-error':''}`} />;
}

React Native/Redux: How to pass down updated state to child components every time state changes?

In React Native and Redux, I am using <NavigationCardStack/> as the root component and render routes with _renderScene(). But seems like whenever the root component re-renders with state update, it does not pass the state down every update, because I put console.log(this.props) in the child component and logs the passed state, but it only logs once and that is the first time the app starts up and never logs after even if the root component re-renders with the state update.
Why isn't it passing down the updated state every time the state changes? And why doesn't the child component re-render whenever the root component does?
This is my set up:
_renderScene (props) {
const { route } = props.scene
return (
<route.component _handleNavigate={this._handleNavigate.bind(this)} state={this.props}/>
)
}
<NavigationCardStack
direction='horizontal'
navigationState={this.props.navigation}
onNavigate={this._handleNavigate.bind(this)}
renderScene={this._renderScene}
renderOverlay={this.renderOverlay}
style={styles.container}
/>
In _renderScene, props alone logs:
And this.props logs the actually state passed down via Redux:
And in the child component childPage.js, I am simply logging like so, and it logs the props passed down (_handleNavigate and state) correctly but the state just continues to represent initial state even if it gets updated:
render() {
console.log(this.props.state)
return (
Thank you in advance!
EDIT
This is my reducer and the child component would just log the initialState here even though other properties have been added and updated:
const initialState = {
meetUp: false,
}
function itemReducer(state = initialState, action) {
switch(action.type) {
case ITEM_QUANTITY:
return {
...state,
quantity: action.quantity
}
case ITEM_PRICE:
return {
...state,
price: action.price
}
case ITEM_MEET_UP:
return {
...state,
meetUp: action.meetUp
}
default:
return state
}
}
export default itemReducer
And connected to the root component like so:
function mapStateToProps(state) {
return {
itemInfo: state.itemReducer,
...
}
}
export default connect(
mapStateToProps,
{
itemQuantity: (value) => itemQuantity(value),
itemPrice: (value) => itemPrice(value),
itemMeetUp: (value) => itemMeetUp(value),
}
)(NavigationRoot)
With following actions:
export function itemMeetUp(value) {
return {
type: ITEM_MEET_UP,
meetUp: value
}
}
export function itemQuantity(value) {
return {
type: ITEM_QUANTITY,
quantity: value
}
}
export function itemPrice(value) {
return {
type: ITEM_PRICE,
price: value
}
}
There is only one reason that a child component does not render after a parent render - its shouldComponentUpdate method, or one from a component between it and the parent, has returned false. Often (particularly with Redux) shouldComponentUpdate methods are written to block the rerender if the properties haven't changed shallowly. Redux relies on you not mutating state, but instead always returning a new object with any changes from your reducer, otherwise the shouldComponentUpdate optimisation causes unexpected behaviour.
Could the problem be that you are modifying deep state rather than returning new objects? See http://redux.js.org/docs/basics/Reducers.html for more details.
A first guess is that your NavigationCardStack isn't rerendering since it thinks the props being presented to it is unchanged. Try forcing a rerender (forceUpdate) and see what happens.

Categories