react setState seems to be cancelled by redux dispatch? - javascript

I'm trying to enable a submit button when I click a checkbox and editing this existing component to use redux (I realize it's not great, I'm just trying to get one part to work before refactoring). For now the button is not on the page.
class Survey extends Component {
state = {
questionsList: [
{
key: 'q1',
checked: this.props.survey && this.props.survey.includes('q1'),
},
{
key: 'q2',
checked: this.props.survey && this.props.survey.includes('q2'),
}
],
};
handleChange = (e) => {
const questions = JSON.parse(JSON.stringify(this.state.questionsList));
// Filter the question for the checkbox user interacted with.
const question = questions.find(q => q.key === e.target.name);
question.checked = !question.checked; // toggle
this.props.setEnableSubmit(questions.some(q => q.checked)); // dispatches a redux action (defined in mapDispatchToProps)
this.setState({ questionsList: questions });
}
render() {
return this.state.questionsList.map(question => (
<Checkbox name={question.key} onChange={this.handleChange} />
));
}
}
const mapStateToProps = (state, ownProps) => ({
survey: state.users[ownProps.match.params.userTarget].survey,
});
const mapDispatchToProps = (dispatch, ownProps) => ({
setEnableSubmit: (value) => {
dispatch(update(ownProps.match.params.userTarget, { enableSubmit: value }));
}
});
export default compose(
withRouter,
connect(
mapStateToProps,
mapDispatchToProps,
),
)(Survey);
I'm trying to store the disabled/enabledness of the button in the store, while keeping all the changes in the local state (and only persisting in the redux store on the button click)
for whatever reason, once enableSubmit happens, in re-render fn this.state.questionsList does not have the updated questionsList anymore (so I can't click the checkboxes). However it works if I remove the dispatch call. It has the same behavior if the setState is not there.
It gets to the setState without an exception. It updates enableSubmit in the store with the value passed. It doesn't seem to matter which line is called first or last or if in a cb to the dispatch or the dispatch is the cb to setState (tried all variations)

I found out that the issue is due to the component rendering this child component. The parent component had a mapStatetoProps watching a slice of the redux store that was getting updated whenever I called enableSubmit, so the parent was receiving new props, rerendering and consequently creating a new child component instance based on our setup. So the multiple mapDispatchToProps calls and seeming ignore of setState was due to a new child component re instantiating

Related

React-Redux connecting two separate component with a toggle

I know this is probably a very basic question, but it's more of a "I don't understand the docs please help me" type of question.
I'm trying to connect two components using React-Redux: the first is a sidebar, and the second is a modal that should appear when clicking on a button in the sidebar. The components are not related in any parent-child relation (except root) so I assume redux is the best option.
I've read all the redux (and react-redux) docs and I understand the core concepts of redux, but I'm having trouble understanding how to implement them in my components.
Basically I want a button in the sidebar that toggles a stored state (true/false is enough) and according to that state the modal would appears (state==true => display:block) and disappear via a button in the modal (state==false => display:none).
What I think I need is an action to toggle a state, for example:
const modalsSlice = createSlice({
name: 'modals',
initalState,
reducers: {
toggleModal(state, action){
state = !state;
}
}
});
then connecting the action in both components (I'm writing the components in classes not as functions) by using:
const toggleModal = {type: 'modals/toggleModal', payload: ''};
const mapStateToProps = state => state.showHideModal;
export default connect(mapStateToProps, toggleModal)(Component);
Now, assuming I'm correct so far, I'm not sure how to continue. I.e. how am I suppose to receive and make the change in the components themselves? Sure, I need to put a function in a button with a onClick={foo} listener but how does the foo suppose to receive and handle the state? And am I suppose to initialize the showHideModal state somewhere? In the root component? While configuring the store?
Any help would be much appreciated.
State Initialisation
You are supposed to initialise the state showHideModal in the slice itself. Moreover, it should be named as either showModal or hideModal for a better interpretation of what this state does.
const modalSlice = createSlice({
name: 'modal',
initialState: {
showModal: false,
},
reducers: {
toggleModal(state){
state.showModal = !state.showModal;
}
}
});
export const { toggleModal } = modalSlice.actions;
SideBar Component
The onClick event handler needs to be passed explicitly via mapDispatchToProps.
import { toggleModal } from './modalSlice';
class Sidebar extends Component {
handleClick = () => {
const { toggleModal } = this.props;
toggleModal();
}
render() {
return (
<div>
{/* rest of JSX */}
<button onClick={this.handleClick}>Toggle Modal</button>
{/* rest of JSX */}
</div>
);
}
}
const mapDispatchToProps = {
toggleModal,
};
export default connect({}, mapDispatchToProps)(Sidebar);
Modal
Note: You cannot access property directly from state like you did state.showHideModal;. You need to access the slice first, followed by property present in it state.modal.showHideModal;.
class Modal extends Component {
handleClick = () => {
const { toggleModal } = this.props;
toggleModal();
}
render() {
const { showModal } = this.props;
return (
<>
{showModal ? (
<div>
<button onClick={this.handleClick}>Close</button>
</div>
) : null}
</>
);
}
}
const mapDispatchToProps = {
toggleModal,
};
const mapStateToProps = state => ({
showModal: state.modal.showModal,
});
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
Update
Coming, to the the reason why Redux throws following warning:
A non-serializable value was detected in an action, in the path: payload
It's because a SyntheticEvent is being passed as a payload to the action. In order to fix this, you need to move the toggleModal call from the onClick prop to a separate handler function. For you reference, check the handleClick function in Modal and SideBar.

Child component doesn't rerender but parent component does rerender. How to make child component rerender?

Parent component does rerender upon receiving new props but its child component doesn't rerender. Child components only render for the first time and never rerender nor receive props from the redux store
I'm getting updated data from redux store in Parent component but not in the child components. Child components only receive data from redux store when they render for the first time
My Parent Component Home.js
Object seaFCLJSON look like this
const seaFCLJSON ={"rates": {"sort":"faster", "someOther": "someOtherValues"}};
when the redux store gets updated, seaFCLJSON looks like this
const seaFCLJSON ={"rates": {"sort":"cheaper","someOther": "someOtherValues"}};
class Home extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
}
};
render() {
const { seaFCLJSON } = this.props;
return (
<>
{!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl ? (
<FCLContainer fclJSON={seaFCLJSON} />
) : null} //it never rerenders upon getting updated data from redux store
<h5>{JSON.stringify(seaFCLJSON.rates && seaFCLJSON.rates.sort)}</h5> //it rerenders everytime upon getting updated data from redux store
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(Home);
isEmpty.js
export const isEmpty = obj => {
return Object.entries(obj).length === 0 && obj.constructor === Object;
};
My Child Component FCLContainer.js
class FCLContainer extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
console.log("outside state value: ", this.state.seaFCLJSON);
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
console.log("inside state value: ", this.state.seaFCLJSON);
}
};
render() {
const { seaFCLJSON } = this.state;
console.log("rendering .. parent props: ", this.props.fclJSON);
console.log("rendering .. redux store props: ", this.props.seaFCLJSON);
return (
<>
<div className="home-result-container">
<div>
<h5>This child component never rerenders :(</h5>
</div>
</div>
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(FCLContainer);
I don't know whether there are problems in Parent component or problems in the child component. componentWillReceiveProps gets invoked in the parent component but not in the child component. Please ignore any missing semi-colon or braces because I have omitted some unnecessary codes.
Edit 1: I just duplicated value from props to state just for debugging purposes.
I will appreciate your help. Thank you.
Edit 2: I was directly changing an object in redux actions. That's the reason CWRP was not getting fired. It was the problem. For more check out my answer below.
componentWillReceiveProps will be deprecated in react 17, use componentDidUpdate instead, which is invoked immediately after updating occurs
Try something like this:
componentDidUpdate(prevProps, prevState) {
if (this.prevProps.seaFCLJSON !== this.props.seaFCLJSON) {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
}
};
At the first place it is absolutely meaningless to duplicate value from props to state, what is the meaning of it? Totally pointless, just keep it in props
About your issue - most probably this condition doesnt match, thats why child component doesnt trigger
!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl
check it in debugger
As far as I can see, your problem is that you pass the following to your child component:
<FCLContainer fclJSON={seaFCLJSON} />
But you assume that you receive a prop called 'seaFCLJSON':
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
You should change your code to:
<FCLContainer seaFCLJSON={seaFCLJSON} />
Apart from that, as #Paul McLoughlin already mentioned, you should use the prop directly instead of adding it to your state.
I found the issue I was directly mutating the object in actions. I only knew state should not be directly mutated in class or inside reducer. I changed the actions where I was directly changing an object and then saving it in redux store via dispatch and, then I received the updated props in CWRP. This really took me a lot of times to figure out. This kind of issue is hard to find out at least for me. I guess I get this from https://github.com/uberVU/react-guide/issues/17
A lesson I learned: Never directly mutate an Object
I changed this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
seaFCLJSON.rates.sort = "Faster"; //this is the main culprit
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
to this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
// seaFCLJSON.rates.sort = "Faster"; //this was the main culprit, got lost
seaFCLJSON = {
...seaFCLJSON,
rates: { ...seaFCLJSON.rates, sort: "Faster" }
};
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
the power of not mutating data
Side note: Redux Troubleshooting

Best approach for using same component for editing and adding data. Mixing component state with redux store

I'm building web app in React with Redux. It is simple device manager. I'm using the same component for adding and updating device in database. I'm not sure, if my approach is correct. Here you can find parts of my solution:
UPDATE MODE:
In componentDidMount I'm checking, if deviceId was passed in url (edit mode). If so, I'm calling redux action to get retrieve data from database. I'm using connect function, so when response arrives, It will be mapped to component props.
Here is my mapStateToProps (probably I should map only specific property but it does not matter in this case)
const mapStateToProps = state => ({
...state
})
and componentDidMount:
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
Next, componentWillReceiveProps will be fired and I will be able to call setState in order to populate inputs in my form.
componentWillReceiveProps(nextProps) {
if (nextProps.devices.item) {
this.setState({
id: nextProps.devices.item.id,
name: nextProps.devices.item.name,
description: nextProps.devices.item.description
});
}
}
ADD MODE:
Add mode is even simpler - I'm just calling setState on each input change.
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
That's how my inputs looks:
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={this.state.description}
/>
I don't like this approach because I have to call setState() after receiving data from backend. I'm also using componentWillReceiveProps which is bad practice.
Are there any better approaches? I can use for example only redux store instead of component state (but I don't need inputs data in redux store). Maybe I can use React ref field and get rid of component state?
Additional question - should I really call setState on each input onChange?
To avoid using componentWillReceiveProps, and because you are using redux, you can do:
class YourComponent extends React.Component {
state = {
// ...
description: undefined,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.description === undefined && nextProps.description) {
return { description: nextProps.description };
}
return null;
}
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
// ...
render() {
let { description } = this.state;
description = description || ''; // use this value in your `TextField`.
// ...
return (
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={description}
/>
);
}
}
const mapStateToProps = (state) => {
let props = { ...state };
const { devices } = state;
if (devices && devices.item) {
props = {
...props,
id: devices.item.id,
name: devices.item.name,
description: devices.item.description,
};
}
return props;
};
export default connect(
mapStateToProps,
)(YourComponent);
You can then access id, name, and description thought this.props instead of this.state. It works because mapStateToProps will be evaluated every time you update the redux store. Also, you will be able to access description through this.state and leave your TextField as is. You can read more about getDerivedStateFromProps here.
As for your second question, calling setState every time the input changes is totally fine; that's what's called a controlled component, and the react team (nor me) encourage its use. See here.

How to update redux store for many components at the same time on click of a single button?

I have four stateful react components in a single page, they get updated simultaneously on the click of a single button.
I now want to use redux store to save the state of all the components.
I wrote react-redux connect for the first component and it works fine.
Then followed the same logic for the second component, but the store is not getting updated along with the first one.
How can I save the states of all the components at same time to store?
I think you can use this kind of structure.
The main page in which you are using the four stateful components should look like this.
class App extends React.Component {
render() {
const { commonState, updateCommonStateHandler } = this.props;
return (
<div>
<Component1 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component2 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component3 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component4 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
</div>
);
}
}
const mapStateToProps = state => {
return {
commonState: state.commonState
};
};
const mapDispatchToProps = dispatch => {
return {
updateCommonStateHandler: change => {
dispatch(() => ({
type: 'UPDATE_COMMON_STATE',
change
}));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
With the reducer
const updateCommonStateReducer = (state = {}, action) => {
const newState = extend({}, state);
if(action.type === 'UPDATE_COMMON_STATE') {
newState.commonState = newState.commonState || {};
extend(newState.commonState, action.change || {});
}
return newState;
};
So if you want to update the state from any child component, you should call the updateCommonStateHandler, which will dispatch an action to update the state.
On state update react will re-render all the components with new commonState.
PS: This is just a sample code explaining the situation, not the solution and it's
written in ES6

Redux state not updating with javascript object

I have this container and component and the object yeast that Im trying to put in my store. However, its not working whenever I try and save it. The object looks like this
{ yeastType : value, temp: value}
Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeast) => {
dispatch(actions.updateYeast(yeast))
}
}
};
const RecipeYeastContainer = connect(
mapStateToProps,
mapDispatchToProps
)(RecipeYeast);
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast)
};
I have no errors in the console or anything. When I open my redux dev tools its telling me the state has already been updated by the time the action is called. And thus only ever saving the first letter thats input into my field. It never persists it. Its really weird. Its also never showing up in the UI. I can type as much as I want and see the action firing and the state keeping the first letter but its not showing up in the input.
Whats weird is that when I change the code to pass in both yeastType and temp to the property function and construct the object in there it works. (See below)
This works: Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeastType, temp) => {
const yeast = {yeastType, temp}
dispatch(actions.updateYeast(yeast))
}
}
};
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast.yeastType, updatedYeast.temp)
};
I cannot figure out why this is happening. I thought I could just pass the object all the way through and not have to reconstruct it.
Do you dispatch your action correctly? And in using redux, you are not updating the state of the component, you're updating the store and then the value in component is from your mapStateToProps function that get from the store. Say it you're updating your store with the object named yourReducer store. ex:
Container.js:
const mapStateToProps = (state) => ({
inputValue: state.yourReducer.value
})
const mapDispatchToProps = (dispatch) => ({
inputHandler: (e) => {
dispatch(yourAction({type: 'CHANGE_INPUT', value: e.target.value}))
// The store catches the action and pass to the reducer that can read the action's type
// in `yourReducer` will catch the 'CHANGE_INPUT' type and
// return a new value as same as the value that passed from the action
}
})
export default connect(mapStateToProps)(Component)
Component.js
export default class Component extends React.Component {
{ your custom function here ... }
render() {
const { inputValue, inputHandler } = this.props
return (
<div>
{/* The input will be the same as the inputValue changed */}
<input value={inputValue} onChange={inputHandler} />
</div>
)
}
}
For debugging redux you can try this redux-devtool.

Categories

Resources