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':''}`} />;
}
Related
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
I have a larger reducer, and I want to add another slice to it that will be fairly complicated unto itself. For example, here's a large, already complex reducer:
const initialState = {
bigchunk1: { ...someObject },
bigchunk2: true,
bigchunk3: [...somevalues],
bigchunk4: 'etc'
}
function mainReducer(state = initialState, action){
switch(action.type){
case Actions.CASE1:
return {
...state,
bigchunk1: {
...state.bigchunk1;
somevalue: action.payload.this_is_already_enough_nesting
}
}
case Actions.CASE2:
return {
...state,
bigchunk4: 'this goes on for awhile'
}
// lots more cases
default:
return state
}
}
I want to add a new slice to state, so the whole initialState would end up in the store looking like this:
const initialState = {
bigchunk1: { ...someObject },
bigchunk2: true,
bigchunk3: [...somevalues],
bigchunk4: 'etc',
newslice: { ...someComplicatedThing }
}
But rather than have to write all my new cases into my original reducer (along with a very messy level of nesting and spread operators), I want to write a new reducer that handles just the cases pertinent to newslice:
const initialState = { ...someComplicatedThing }
function newSliceReducer(state = initialState, action){
switch(action.type}{
// cases here
}
}
I am aware of combineReducers, but I'm having a hard time thinking of how to apply that in this scenario. combineReducers can take these two reducers and place them as siblings, but how can I combine these so that newSliceReducer and its associated state becomes a child of mainReducer under the name newslice? I feel like this should be simple, but its escaping me right now. I have been reading the redux docs, but I'm not seeing the answer clearly. I do not want to use redux-toolkit or any outside libraries.
In the default switch case, you can do
default:
let newSlice = newSliceReducer(state.newslice, action)
if (newSlice !== state.newslice) {
return {...state,newslice:newSlice}
}
return state
You can think of Redux as a coding pattern instead of library.
A reducer's job is to update the state and return new object if changed else return old object. If a new object is returned, react assumes something changed and a refresh is triggered.
Now, what you have is a "newSliceReducer" whose state is just a part of main reducer. So, I followed above rules and called the new reducer with state as smaller part of the original state and checked the result if it changed. If yes, I created a new object with updated state else return old state.
It is important to check if newSlice actually changed.
According to the Redux documentation, it seems to be a standard practice to set an initialState on your reducer. However this initialState needs to be maintained and if the state is being populated based on an API response, then you may have the initial state out of sync with the API response. This is especially true in cases where the state is made up of nested objects.
Is it to avoid null-checking (sometimes the initial state is set to null), are there any performance benefits? Does it improve code readability?
Taken from Redux docs:
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
Then in our reducer we may have an action which replaces the value in the state (for example based on an API response). Such as:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_TODOS:
return Object.assign({}, state, {
todos: action.todos
})
default:
return state
}
}
However the same behaviour may be achieved without using an initialState, by checking the state in the component (or selector).
const MyComponent = ({todos}) => {
if (!todos) { // if we do not have an initialState, todos will be undefined if SET_TODOS hasn't been called
return null;
}
return <div>{todos.map(n => ...)}</div>
}
If the API returns a new property (notes), we would need to update as follows:
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: [],
notes: [] // <-----
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_TODOS:
return Object.assign({}, state, {
todos: action.todos,
})
case SET_NOTES:
return Object.assign({}, state, {
notes: action.notes,
})
default:
return state
}
}
This is further complicated in cases when you have nested objects. If the todos has a child property subtasks: [], why are we not setting an initial state for it?
I think it's more of a convenience than anything else. I highly doubt there is any performance implications. Also while in my personal opinion it is cleaner to add all the properties you expect upfront, you are not really required to add it to the initial state. You can simply add it when returning the new state, obviously if you're using typescript this is a different story.
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
// notes: [] you don't necessarily need to add it to the initial state
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_TODOS:
return Object.assign({}, state, {
todos: action.todos,
notes: action.notes // <-----
})
default:
return state
}
}
Having an initial state helps to have "cleaner" code. Like Brian Thompson said in the comments, code is more predictable if your data structure stays consistent.
It is also recommended (by some ppl) to avoid having multiples returns in one function. It may not be the best perf-wise, but it might be the easiest to read.
So having to do an early return in your component might not be the cleanest way to achieve the behavior your want.
That being said, if your implementation works, why not use it ? Well i think it's best when working as a team to stick to the conventions as much as possible.
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
I am unable to get props inside constructor that I have implemented using redux concept.
Code for container component
class UpdateItem extends Component{
constructor(props) {
super(props);
console.log(this.props.item.itemTitle) // output: undefined
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
//If the input fields were directly within this
//this component, we could use this.refs.[FIELD].value
//Instead, we want to save the data for when the form is submitted
let state = {};
state[e.target.name] = e.target.value.trim();
this.setState(state);
}
handleSubmit(e) {
//we don't want the form to submit, so we pritem the default behavior
e.preventDefault();
let errors = {};
errors = this._validate();
if(Object.keys(errors).length != 0) {
this.setState({
errors: errors
});
return;
}
let itemData = new FormData();
itemData.append('itemTitle',this.state.itemTitle)
this.props.onSubmit(itemData);
}
componentDidMount(){
this.props.getItemByID();
}
componentWillReceiveProps(nextProps){
if (this.props.item.itemID != nextProps.item.itemID){
//Necessary to populate form when existing item is loaded directly.
this.props.getItemByID();
}
}
render(){
let {item} = this.props;
return(
<UpdateItemForm
itemTitle={this.state.itemTitle}
errors={this.state.errors}
/>
);
}
}
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
function mapStateToProps(state, ownProps){
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
function mapDispatchToProps (dispatch, ownProps) {
return {
getItemByID:()=>dispatch(loadItemByID(ownProps.params.id)),
onSubmit: (values) => dispatch(updateItem(values))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(UpdateItem);
Inside render() method am able to get the props i.e. item from the redux but not inside constructor.
And code for the actions to see if the redux implementation correct or not,
export function loadItemByID(ID){
return function(dispatch){
return itemAPI.getItemByID(ID).then(item => {
dispatch(loadItemByIDSuccess(item));
}).catch(error => {
throw(error);
});
};
}
export function loadItemByIDSuccess(item){
return {type: types.LOAD_ITEM_BY_ID_SUCCESS, item}
}
Finally my reducer looks as follows,
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
I have googled to get answers with no luck, I don't know where i made a mistake. If some one point out for me it would be a great help. Thanks in advance.
The reason you can't access the props in the constructor is that it is only called once, before the component is first mounted.
The action to load the item is called in the componentWillMount function, which occurs after the constructor is called.
It appears like you are trying to set a default value in the mapStateToProps function but aren't using it at all
function mapStateToProps(state, ownProps){
// this is never used
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
The next part I notice is that your are taking the state from redux and trying to inject it into the component's local state
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
Mixing redux state and component state is very rarely a good idea and should try to be avoided. It can lead to inconsistency and and hard to find bugs.
In this case, I don't see any reason you can't replace all the uses of this.state.itemTitle with this.props.items.itemTitle and remove it completely from the component state.
Observations
There are some peculiar things about your code that make it very difficult for me to infer the intention behind the code.
Firstly the reducer
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
You haven't shown the initialState object, but generally it represents the whole initial state for the reducer, so using initialState.item stands out to me. You may be reusing a shared initial state object for all of the reducers so I'm not too concerned about this.
What is very confusing the Object.assign call. I'm not sure it the intention is to output an object replacing item in the state, or if it is to append action.item to an array, or to have an array with a single item as the resulting state. The state = action.item part is also particularly puzzling as to it's intention in the operation.
This is further confused by the PropTypes for UpdateItem which requires item to be an array
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
But the usage in the component treats it like and object
this.state = {
// expected some kind of array lookup here |
// V---------------
itemTitle: this.props.item.itemTitle,
errors: {}
};
Update from comments
Here is a example of what I was talking about in the comments. It's a simplified version of your code (I don't have all your components. I've also modified a few things to match my personal style, but hopefully you can still see what's going on.