Related
I am using MERN and Redux.
I have a clickHandler function that calls a findAuthor function which is imported from my actions. This finds a user by their id and returns it. I have added the user to the global state. I want to then retrieve the user and add their name to local state but i can't get this working. I keep getting this error TypeError: this.props.subAuthor is undefined. What am i missing here? When i try just printing to console i get no object showing until the second click. How do i get it t update straight away?
import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchSubjects } from "../../actions/subject";
import { fetchComments } from "../../actions/comment";
import { updateSubject } from "../../actions/subject";
import { getUser } from "../../actions/authActions";
class Subject extends Component {
// on loading the subjects and comments
// are fetched from the database
componentDidMount() {
this.props.fetchSubjects();
this.props.fetchComments();
}
constructor(props) {
super(props);
this.state = {
// set inital state for subjects
// description, summary and comments all invisible
viewDesription: -1,
viewSummary: -1,
comments: [],
name: "",
};
}
componentWillReceiveProps(nextProps) {
// new subject and comments are added to the top
// of the arrays
if (nextProps.newPost) {
this.props.subjects.unshift(nextProps.newPost);
}
if (nextProps.newPost) {
this.props.comments.unshift(nextProps.newPost);
}
}
clickHandler = (id) => {
// when a subject title is clicked pass in its id
// and make the description and comments visible
const { viewDescription } = this.state;
this.setState({ viewDescription: viewDescription === id ? -1 : id });
// add relevant comments to the state
var i;
var temp = [];
for (i = 0; i < this.props.comments.length; i++) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
this.setState({
comments: temp,
});
// save the subject id to local storage
// this is done incase a new comment is added
// then the subject associated with it can be retrieved
// and added as a property of that comment
localStorage.setItem("passedSubject", id);
//testing getUser
this.findAuthor(id); // this updates the tempUser in state
this.setState({ name: this.props.subAuthor.name });
};
// hovering on and off subjects toggles the visibility of the summary
hoverHandler = (id) => {
this.setState({ viewSummary: id });
};
hoverOffHandler = () => {
this.setState({ viewSummary: -1 });
};
rateHandler = (id, rate) => {
const subject = this.props.subjects.find((subject) => subject._id === id);
// when no subject was found, the updateSubject won't be called
subject &&
this.props.updateSubject(id, rate, subject.noOfVotes, subject.rating);
alert("Thank you for rating this subject.");
};
// take in the id of the subject
// find it in the props
// get its author id
// call the getUser passing the author id
findAuthor(id) {
console.log("Hitting findAuthor function");
const subject = this.props.subjects.find((subject) => subject._id === id);
const authorId = subject.author;
console.log(authorId);
this.props.getUser(authorId);
}
render() {
const subjectItems = this.props.subjects.map((subject) => {
// if the state equals the id set to visible if not set to invisible
var view = this.state.viewDescription === subject._id ? "" : "none";
var hover = this.state.viewSummary === subject._id ? "" : "none";
var comments = this.state.comments;
var subjectAuthor = this.state.name;
return (
<div key={subject._id}>
<div className="subjectTitle">
<p
className="title"
onClick={() => this.clickHandler(subject._id)}
onMouseEnter={() => this.hoverHandler(subject._id)}
onMouseLeave={() => this.hoverOffHandler()}
>
{subject.title}
</p>
<p className="rate">
Rate this subject:
<button onClick={() => this.rateHandler(subject._id, 1)}>
1
</button>
<button onClick={() => this.rateHandler(subject._id, 2)}>
2
</button>
<button onClick={() => this.rateHandler(subject._id, 3)}>
3
</button>
<button onClick={() => this.rateHandler(subject._id, 4)}>
4
</button>
<button onClick={() => this.rateHandler(subject._id, 5)}>
5
</button>
</p>
<p className="rating">
Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
</p>
<p className="summary" style={{ display: hover }}>
{subject.summary}
</p>
</div>
<div className="subjectBody " style={{ display: view }}>
<div className="subjectAuthor">
<p className="author">
Subject created by: {subjectAuthor}
<br /> {subject.date}
</p>
</div>
<div className="subjectDescription">
<p className="description">{subject.description}</p>
</div>
<div className="subjectLinks">Links:</div>
<div className="subjectComments">
<p style={{ fontWeight: "bold" }}>Comments:</p>
{comments.map((comment, i) => {
return (
<div key={i} className="singleComment">
<p>
{comment.title}
<br />
{comment.comment}
<br />
Comment by : {comment.author}
</p>
</div>
);
})}
<a href="/addcomment">
<div className="buttonAddComment">ADD COMMENT</div>
</a>
</div>
</div>
</div>
);
});
return (
<div id="Subject">
<GoogleSearch />
{subjectItems}
</div>
);
}
}
Subject.propTypes = {
fetchSubjects: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
updateSubject: PropTypes.func.isRequired,
getUser: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
newPost: PropTypes.object,
subAuthor: PropTypes.object,
};
const mapStateToProps = (state) => ({
subjects: state.subjects.items,
newSubject: state.subjects.item,
comments: state.comments.items,
newComment: state.comments.item,
subAuthor: state.auth.tempUser[0],
});
// export default Subject;
export default connect(mapStateToProps, {
fetchSubjects,
fetchComments,
updateSubject, // rate subject
getUser, // used for getting author name
})(Subject, Comment);
I'd like to offer an alternative solution to the current code you have been writing so far. I know this is not codereview (and it wouldn't be on topic there, unless it is actually working code), but still, I would like to show you a different way of dividing up your components.
From what I see, you have many components, currently all jampacked in to one very large component. This can complicate things on the long run, and if you can, you should avoid it.
As I see it from the code you have posted, you really have several components, which I divided in:
Subject
Comment
User
Rating
RatingViewer
By dividing your now large component, you are making it easier to handle the data for one component at a later time and reuse the components you are making. You might want to reuse some of these components.
For the purpose of an alternative solution, I created a very quick and basic demo on how you might refactor your code. This is only a suggestion, in the hope that it will also solve your current problem.
The problem you are having is that you want to load that data, and use it directly. Any fetch operation is however asynchronous, so after you have called this.props.getUser(authorId); your author gets added somewhere in your state, but it will not be available until fetching has been completed and your component gets re-rendered.
I hope the information in the demo can give you some insight, it might not be exactly matching your scenario, but it should give you an indication of what you could do differently.
// imports
const { Component } = React;
const { Provider, connect } = ReactRedux;
const { render } = ReactDOM;
const { createStore, combineReducers } = Redux;
// some fake db data
const db = {
comments: [
{ id: 1, subject: 2, user: 2, comment: 'Interesting book' },
{ id: 2, subject: 2, user: 3, comment: 'Is interesting the only word you know, you twit' }
],
subjects: [
{
id: 1,
title: 'Some interesting title',
summary: 'Some interesting summary / plot point',
author: 2,
rate: 0,
noOfVotes: 0
},
{
id: 2,
title: 'Some less interesting title',
summary: 'Some more interesting summary / plot point',
author: 1,
rate: 5,
noOfVotes: 2
}
],
authors: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' }
],
users: [
{ id: 1, name: 'user 1' },
{ id: 2, name: 'user 2' },
{ id: 3, name: 'user 3' }
]
};
// reducers
const authorReducer = ( state = {}, action ) => {
switch (action.type) {
case 'author/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const userReducer = ( state = {}, action ) => {
switch (action.type) {
case 'user/add':
return { ...state, [action.payload.id]: action.payload };
default:
return state;
}
};
const subjectReducer = ( state = {}, action ) => {
switch (action.type) {
case 'subject/retrieved':
return Object.assign( {}, ...action.payload.map( subject => ({ [subject.id]: subject }) ) );
case 'subject/add':
return { ...state, [action.payload.id]: action.payload };
case 'subject/update':
const { id } = action.payload;
return { ...state, [id]: action.payload };
default:
return state;
}
};
const commentReducer = ( state = [], action ) => {
switch (action.type) {
case 'comment/retrieved':
return action.payload.slice();
case 'comments/add':
return [...state, action.payload ];
default:
return state;
}
};
// create the store
const store = createStore( combineReducers({
users: userReducer,
authors: authorReducer,
comments: commentReducer,
subjects: subjectReducer
}) );
// some promise aware fetch methods
const fakeFetch = (entity, filter = null) => {
const entities = db[entity];
return Promise.resolve( (filter ? entities.filter( filter ) : entities).map( e => ({...e}) ) );
}
const fakeUpdate = (entity, id, updatedValue ) => {
const targetEntity = db[entity].find( e => e.id === id );
if (!targetEntity) {
return Promise.reject();
}
Object.assign( targetEntity, updatedValue );
return Promise.resolve( { ...targetEntity } );
}
// separate components
class App extends Component {
render() {
return <Subjects />;
}
}
// subjects component
// cares about retrieving the subjects and displaying them
class SubjectsComponent extends Component {
componentDidMount() {
this.props.fetchSubjects();
}
render() {
const { subjects } = this.props;
if (!subjects || !subjects.length) {
return <div>Loading</div>;
}
return (
<div>
{ subjects.map( subject => <Subject key={subject.id} subject={subject} /> ) }
</div>
);
}
}
// subject component
// displays a subject and fetches the comments for "all" subjects
// this should probably only fetch its own comments, but then reducer has to be changed aswell
// to be aware of that
class SubjectComponent extends Component {
componentDidMount() {
this.props.fetchComments();
}
render() {
const { subject } = this.props;
return (
<div className="subject">
<h1>{ subject.title }<RateView subject={subject} /></h1>
<p>{ subject.summary }</p>
<Rate subject={subject} />
<h2>Comments</h2>
{ this.props.comments && this.props.comments.map( comment => <Comment key={comment.id} comment={comment} /> ) }
</div>
);
}
}
// Just displays a comment and a User component
const Comment = ({ comment }) => {
return (
<div className="comment">
<p>{ comment.comment }</p>
<User id={comment.user} />
</div>
);
}
// User component
// fetches the user in case he hasn't been loaded yet
class UserComponent extends Component {
componentDidMount() {
if (!this.props.user) {
this.props.fetchUser( this.props.id );
}
}
render() {
return <span className="user">{ this.props.user && this.props.user.name }</span>;
}
}
// shows the current rating of a post
const RateView = ({ subject }) => {
if (subject.noOfVotes === 0) {
return <span className="rating">No rating yet</span>;
}
const { rate, noOfVotes } = subject;
return <span className="rating">Total rating { (rate / noOfVotes).toFixed(1) }</span>;
}
// enables voting on a subject, can be triggered once per rendering
// this should truly be combined with the user who rated the subject, but it's a demo
class RateComponent extends Component {
constructor() {
super();
this.onRateClicked = this.onRateClicked.bind( this );
this.state = {
hasRated: false,
rateValue: -1
};
}
onRateClicked( e ) {
const userRate = parseInt( e.target.getAttribute('data-value') );
const { subject } = this.props;
this.setState({ hasRated: true, rateValue: userRate }, () => {
this.props.updateRate( { ...subject, rate: subject.rate + userRate, noOfVotes: subject.noOfVotes + 1 } );
});
}
render() {
if (this.state.hasRated) {
return <span className="user-rate">You rated this subject with { this.state.rateValue }</span>;
}
return (
<div>
{ [1, 2, 3, 4, 5].map( value => <button type="button" onClick={ this.onRateClicked } data-value={value} key={value}>{ value }</button> ) }
</div>
);
}
}
// connecting all the components to the store, with their states and dispatchers
const Subjects = connect(
state => ({ subjects: Object.values( state.subjects ) }),
dispatch => ({
fetchSubjects() {
return fakeFetch('subjects').then( result => dispatch({ type: 'subject/retrieved', payload: result }) );
}
}))( SubjectsComponent );
// ownProps will be used to filter only the data required for the component that it is using
const Subject = connect(
(state, ownProps) => ({ comments: state.comments.filter( comment => comment.subject === ownProps.subject.id ) }),
dispatch => ({
fetchComments() {
return fakeFetch('comments' ).then( result => dispatch({ type: 'comment/retrieved', payload: result }) );
}
}))( SubjectComponent );
const User = connect(
(state, ownProps) => ({ user: state.users[ownProps.id] }),
dispatch => ({
fetchUser( id ) {
return fakeFetch('users', user => user.id === id).then( result => dispatch({ type: 'user/add', payload: result[0] }) );
}
}))( UserComponent );
const Rate = connect( null, dispatch => ({
updateRate( updatedSubject ) {
return fakeUpdate('subjects', updatedSubject.id, updatedSubject).then( updated => dispatch({ type: 'subject/update', payload: updated }) );
}
}))( RateComponent );
// bind it all together and run the app
const targetElement = document.querySelector('#container');
render( <Provider store={store}><App /></Provider>, targetElement );
.user {
font-style: italic;
font-size: .9em;
}
.comment {
padding-left: 10px;
background-color: #efefef;
border-top: solid #ddd 1px;
}
h1, h2 {
font-size: .8em;
line-height: .9em;
}
.rating {
padding: 5px;
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js" integrity="sha512-SUJujhtUWZUlwsABaZNnTFRlvCu7XGBZBL1VF33qRvvgNk3pBS9E353kcag4JAv05/nsB9sanSXFbdHAUW9+lg==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js" integrity="sha512-SYsXmAblZhruCNUVmTp5/v2a1Fnoia06iJh3+L9B9wUaqpRVjcNBQsqAglQG9b5+IaVCfLDH5+vW923JL5epZA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.1/react-redux.min.js" integrity="sha512-Ae6lzX7eAwqencnyfCtoAf2h3tQhsV5DrHiqExqyjKrxvTgPHwwOlM694naWdO2ChMmBk3by5oM2c3soVPbI5g==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha512-P36ourTueX/PrXrD4Auc1kVLoTE7bkWrIrkaM0IG2X3Fd90LFgTRogpZzNBssay0XOXhrIgudf4wFeftdsPDiQ==" crossorigin="anonymous"></script>
<div id="container"></div>
I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!
I have a lot of hits, which I want to add to an array once a hit is pressed. However, as far as I observed, the array looked like it got the name of the hit, which is the value. The value was gone in like half second.
I have tried the methods like building constructor, and doing things like
onClick={e => this.handleSelect(e)}
value={hit.name}
onClick={this.handleSelect.bind(this)}
value={hit.name}
onClick={this.handleSelect.bind(this)}
defaultValue={hit.name}
and so on
export default class Tagsearch extends Component {
constructor(props) {
super(props);
this.state = {
dropDownOpen:false,
text:"",
tags:[]
};
this.handleRemoveItem = this.handleRemoveItem.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleTextChange = this.handleTextChange.bind(this);
}
handleSelect = (e) => {
this.setState(
{ tags:[...this.state.tags, e.target.value]
});
}
render() {
const HitComponent = ({ hit }) => {
return (
<div className="infos">
<button
className="d-inline-flex p-2"
onClick={e => this.handleSelect(e)}
value={hit.name}
>
<Highlight attribute="name" hit={hit} />
</button>
</div>
);
}
const MyHits = connectHits(({ hits }) => {
const hs = hits.map(hit => <HitComponent key={hit.objectID} hit={hit}/>);
return <div id="hits">{hs}</div>;
})
return (
<InstantSearch
appId="JZR96HCCHL"
apiKey="b6fb26478563473aa77c0930824eb913"
indexName="tags"
>
<CustomSearchBox />
{result}
</InstantSearch>
)
}
}
Basically, what I want is to pass the name of the hit component to handleSelect method once the corresponding button is pressed.
You can simply pass the hit.name value into the arrow function.
Full working code example (simple paste into codesandbox.io):
import React from "react";
import ReactDOM from "react-dom";
const HitComponent = ({ hit, handleSelect }) => {
return <button onClick={() => handleSelect(hit)}>{hit.name}</button>;
};
class Tagsearch extends React.Component {
constructor(props) {
super(props);
this.state = {
tags: []
};
}
handleSelect = value => {
this.setState(prevState => {
return { tags: [...prevState.tags, value] };
});
};
render() {
const hitList = this.props.hitList;
return hitList.map(hit => (
<HitComponent key={hit.id} hit={hit} handleSelect={this.handleSelect} />
));
}
}
function App() {
return (
<div className="App">
<Tagsearch
hitList={[
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" }
]}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
additionally:
note the use of prevState! This is a best practice when modifying state. You can google as to why!
you should define the HitComponent component outside of the render method. it doesn't need to be redefined each time the component is rendered!
Expected effect: click button -> call function setEditing() -> call function item() inside setEditing() -> this.state.isEditing changes to true -> in parent this.state.isEdit changes to true. When I call the item () function, the value of isEditing does not change
App
class App extends React.Component {
constructor() {
super();
this.state = {
isEdit = false;
};
}
handleSomething = (value) => {
this.setState(prevState => {
return {
isEdit: value
};
});
}
render() {
return (
<div>
<ul>
{
this.state.todos
.map((todo, index) =>
<Todo
key={index}
index={index}
todo={todo}
handleSomething={this.handleSomething}
/>
)
}
</ul>
</div>
);
}
}
Todo
class Todo extends Component {
state = {
isEditing: false
}
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
})
this.item();
}
item = () => {
const { isEditing} = this.state;
this.props.handleSomething(isEditing);
}
render() {
return (
<button onClick={() => this.setEditing()}>Edit</button>
)
}
}
You'll need to call this.item after the state was changed, something like
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
}, this.item)
}
Also, if you want to derive a new state form the old one, you'll have to use something like this:
setEditing = () => {
this.setState(prevState => ({
isEditing: !prevState.isEditing
}), this.item)
}
Try basing your state change on the previous state, and call parent function in a callback :
setEditing = () => {
this.setState(prevState => ({
isEditing: !prevState.isEditing
}), this.item)
}
Because as written in the React doc :
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
(https://reactjs.org/docs/react-component.html#setstate)
class Todo extends React.Component {
state = {
isEditing: false
}
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
},this.item())
}
item = () => {
const { isEditing} = this.state;
this.props.handleSomething(isEditing);
}
render() {
return (
<button onClick={() => this.setEditing()}>
Edit
</button>
)
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
isEdit : false,
todos : [
"test 1",
"test 2"
]
};
}
handleSomething = (value) => {
this.setState(prevState => {
return {
isEdit: value
};
});
}
render() {
return (
<div>
<ul>
{
this.state.todos
.map((todo, index) =>
<Todo
key={index}
index={index}
todo={todo}
handleSomething={this.handleSomething}
/>
)
}
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I have a list and calendar view component in my parent component. For the calendar component I want to be able to push search filters to my url for filtering out the unselected locations. I'm trying to generate a querystring based on the parameters I give to my queryString function, but when I push the queryString to my url I get the following error:
A state mutation was detected between dispatches, in the path locations.calendarLocationList.0. This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)
I'm not sure what is causing this, since I haven't touched the state during this process.
Parent component, rendering list and calendar view
class LocationShell extends Component<
LocationShellProps & WithNamespaces & RouteComponentProps,
LocationShellState
> {
constructor(props: LocationShellProps & WithNamespaces & RouteComponentProps) {
super(props);
this.state = {
isCalendarView: false,
open: false,
locationIdToDelete: -1,
activePage: 1,
activeSortHeader: undefined,
direction: 'ascending',
searchValue: undefined
};
}
componentDidMount = (
{ loadLocations, loadSessionLocations, loadCalendarListLocations } = this.props,
{ activePage } = this.state
) => {
loadLocations({ page: activePage });
loadSessionLocations();
loadCalendarListLocations();
};
toggleView = () => {
const { isCalendarView } = this.state;
this.setState((prevState) => ({
...prevState,
isCalendarView: !isCalendarView
}))
}
renderListView = () => {
const { locationStatus, locations, selectedLocationId, history, match, pages, t } = this.props;
const { activePage, activeSortHeader, direction } = this.state;
switch (locationStatus) {
case ProgressStatus.InProgress:
return <InitialLoader />
case ProgressStatus.Done:
return (
<DataTableWrapper
// props
/>
)
case ProgressStatus.Error:
return <NoDataFound />
case ProgressStatus.Uninitialized:
return null
}
}
renderCalendarView = ({ calendarListStatus, sessionLocations, calendarListLocations } = this.props) => {
switch (calendarListStatus) {
case ProgressStatus.InProgress:
return <InitialLoader />
case ProgressStatus.Done:
const events = toLocationEvents(sessionLocations!);
return <CalendarView {...this.props} events={events} items={calendarListLocations!} name={'locations'} />
case ProgressStatus.Error:
return <NoDataFound />
case ProgressStatus.Uninitialized:
return null
}
}
render() {
const { pathName, t } = this.props;
const { isCalendarView } = this.state;
return (
<Fragment>
<PageHeader
breadCrumbParts={split(pathName, '/').map(x => t(x))}
title={t('moduleTitle')}
/>
<Button.Group size="mini" style={{ padding: '10px 5px 10px 0px' }}>
<Button positive={!isCalendarView} onClick={this.toggleView}>Lijst</Button>
<Button.Or />
<Button positive={isCalendarView} onClick={this.toggleView}>Kalender</Button>
</Button.Group>
<Button
positive
icon='add'
size="mini"
labelPosition='right'
content="Nieuwe locatie"
onClick={() => this.props.history.push(this.props.match.path + '/create')}
/>
{isCalendarView ? this.renderCalendarView() : this.renderListView()}
</Fragment>
);
}
}
const mapStateToProps = (state: GlobalState) => {
return {
locations: getLocations(state.locations),
calendarListLocations: state.locations.calendarLocationList,
calendarListStatus: state.locations.calendarListStatus,
sessionLocations: state.locations.sessionLocations,
selectedLocation: getSelectedLocation(state.locations),
selectedLocationId: getSelectedLocationId(state.locations),
pages: getAmountPages(state.locations),
locationStatus: state.locations.locationStatus,
sessionLocationStatus: state.locations.sessionLocationStatus,
pathName: getPathName(state.router)
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
loadLocations: (queryParams: QueryParams) =>
dispatch(FetchLocations(queryParams)),
loadSessionLocations: () => dispatch(FetchTrainingSessionLocations({})),
loadCalendarListLocations : () => dispatch(FetchCalendarListLocations({})),
clearLocations: () => dispatch(ClearLocations()),
deleteLocation: (id: number) => dispatch(DeleteLocation({ locationId: id }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(withNamespaces('locations')(LocationShell));
renderCalendarView() is rendering my calendar component
My calendar Component:
interface CalendarViewState {
selectedIds: number[];
}
type CalendarViewProps = {
events: CalendarEvent[];
name: string;
items: CalendarListLocation[];
navigatePush: (values: string) => void;
} & RouteComponentProps
class CalendarView extends Component<CalendarViewProps & WithNamespaces, CalendarViewState> {
state: CalendarViewState = {
selectedIds: []
}
componentDidMount = () => {
const { events, items } = this.props;
const { baseUrl, newEntity } = moduleConstants;
this.setState((prevState) => ({
...prevState,
selectedIds: items.map(x => x._id)
}), () => {
updateQueryString(this.props, { page: 1, locations: [1, 2] })
}
)
}
queryParams(props: CalendarViewProps = this.props) {
return queryParams<QueryParams>(props.location.search);
}
componentDidUpdate = (prevProps: CalendarViewProps, prevState: CalendarViewState) => {
const { selectedIds } = this.state;
console.log()
if (!isEqual(prevState.selectedIds, selectedIds)) {
console.log(this.queryParams())
}
}
handleChange = (id: number) => {
const { selectedIds } = this.state;
this.setState((prevState) => ({
...prevState,
selectedIds: (selectedIds.includes(id) ? selectedIds.filter(x => x !== id) : [...selectedIds, id])
}));
};
render() {
const { events, name, t, items } = this.props
return (
<Grid divided="vertically" padded>
<Grid.Row columns={2}>
<Grid.Column width={4}>
<CalendarSelectionList
name={t(name)}
onSelectionChange={this.handleChange}
selectedIds={this.state.selectedIds}
locations={items.sort((a: CalendarListLocation, b: CalendarListLocation) => a.name.localeCompare(b.name))}
/>
</Grid.Column>
<Grid.Column width={12}>
<div style={{ height: '800px' }}>
<Calendar
events={events.filter(x => this.state.selectedIds.includes(x.id))}
/>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
);
}
}
const mapDispatchToProps = (dispatch: Dispatch) => ({
navigatePush: (path: string) => dispatch(push(path))
});
export default connect(
null,
mapDispatchToProps
)(withNamespaces(['calendar'])(CalendarView));
updateQueryString(this.props, { page: 1, locations: [1, 2] }) gets fired, this function will update the url with the generated queryString
export function queryParams<T>(search: string) {
return (queryString.parse(search) as unknown) as T;
}
export function updateQueryString<T>(props: RouteComponentProps, newValues: T) {
const currentQuery = queryParams<T>(props.location.search);
const newProps = Object.assign(currentQuery, newValues);
props.history.push({
pathname: props.location.pathname,
search: queryString.stringify(filterSearchResults(newProps))
});
}
function filterSearchResults(values: any) {
let obj: any = {};
Object.keys(values).forEach(
key => values[key] && (obj[key] = values[key])
);
return obj;
}
After this, the above error occurs. Why is this error occuring?
The error means that locations.calendarLocationList was mutated, while Redux store is supposed to be immutable.
calendarLocationList is used as items in CalendarView and mutated with items.sort(...) because array sort mutates existing array instead of creating a new one.
This can be fixed with [...items].sort(...).