0

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().

Community
  • 1
  • 1
dwjohnston
  • 11,163
  • 32
  • 99
  • 194
  • Redux state should be immutable! You have to understand why you action does not trigger a render call. – Moti Korets Apr 12 '18 at 03:12
  • @MotiKorets I might have used the wrong word. Clearly redux state can be updated. The reason it doesn't trigger a render call is because, for example if `state.hasError === false` and I try reset it to false again with `Object.assign` redux won't detect it. – dwjohnston Apr 12 '18 at 03:17
  • i"m talking about the code. `setError` clearly mutates state which should never be done. Reducer is a function which receives old state and action and returns new state. In any case the component has inner state so i don't really get it why you need it to be aware of redux at all. – Moti Korets Apr 12 '18 at 03:39
  • @MotiKorets That's just the reducer function. I'll add a bit more for clarity. – dwjohnston Apr 12 '18 at 03:45
  • I think the complete component code would be helpful. And consider rewrite it without redux as i feel it only gets in your way here. – Moti Korets Apr 12 '18 at 04:22

1 Answers1

2

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':''}`} />; 
}
Matan Bobi
  • 2,693
  • 1
  • 15
  • 27
  • Hi, thanks for your reply. 1. I'm not actually manipulating the state directly. Note that there's the object.assign in the allReducers method. 2. Thanks for pointing that out. It looks like I should be using `getDerivedStateFromProps` anyway. 3. The point of mentioning state is because it's the random number that causes the rerender, not the hasError property. – dwjohnston Apr 12 '18 at 21:14
  • @dwjohnston Actually, in this case I would probably use `componentDidUpdate` since your'e creating side effects for the component(setTimeout). You can have a look at my answer to see when should you put such a logic here: https://stackoverflow.com/questions/49772726/what-is-the-correct-pattern-in-react-js-to-invoke-a-component-method-on-specific/49777146#49777146 Regarding your problem, I think that some code is missing to figure it out. Where are you updating your `hasError` state to `false` ? – Matan Bobi Apr 13 '18 at 14:13
  • When I get the time, I'll create a full solution on codepen. – dwjohnston Apr 14 '18 at 11:19