How do I setState of a deeply nested property in React? - reactjs

I'm trying to increment the tally property of a given habit, in this context I'm targeting tally: 4. On a related note, I'm open to suggestions for better ways of structuring my data.
class App extends Component {
constructor(props){
super(props)
this.state = {
displayedHabit: "Exercise",
currentUser: "Achilles",
userList: [
{
user: "Achilles",
id: 0,
habits: [
{
habit: "Exercise",
tally: 123
},
{
habit: "Eat Vegetables",
tally: 4
},
]
}
]
}
}
Here is the implementation I've tried after searching for solutions. I don't believe it's working because assignments that use .find() that work after mounting are broken after I call the increment function through an event handler - leading me to believe .find() is no longer being called on an array.
increment = () => {
let newCount = this.state.userList[0].habits[1].tally + 1
this.setState({
userList: {
...this.state.userList[0].habits[1],
tally: newCount
}
})
}

In React, it's very important that you don't mutate your data structures. This essentially means that you have to copy the data structure at each level of your nested data. Since this is too much work, I suggest you use a library made for this purpose. I personally use object-path-immutable.
Example:
import {set} from 'object-path-immutable';
increment = ({userIndex, habitIndex}) => this.setState({
userList: set(this.state.userList, [userIndex, 'habits', habitIndex, 'tally'], this.state.userList[userIndex].habits[habitIndex].tally + 1)
});

I would recommend restructuring your data such that you have objects which actually map the data in accessible ways ie:
this.state = {
displayedHabit: "Exercise",
currentUser: "Achilles",
userList: {
"Achilles": { // Could also be id
id: 0,
habits: {
"Exercise": 123,
"EatVegetables": 4
}
}
}
}
This would allow you to do something like
increment = () => {
const {userList} = this.state;
this.setState({
userList: {
...userList,
Achilles: {
...userList.Achilles
habits: {
...userlist.Achilles.habits
'EatVegetables': userlist.Achilles.habits.EatVegetables + 1
}
}
}
})
}
This would be further simplified by using object-path-immutable which would allow you to do something simple like:
increment = () => {
const {userList} = this.state;
this.setState({
userList: immutable.set(userList, 'Achilles.id.habits.Exercise', userList.Achilles.id.habits.Exercise + 1)
})
}
In order to make this more generic though I would recommend doing what something similar to what earthling suggested:
import {set} from 'object-path-immutable';
incrementUserHabit = ({userIndex, habitIndex}) => this.setState({
userList: set(this.state.userList, [userIndex, 'habits', habitIndex, 'tally'], this.state.userList[userIndex].habits[habitIndex].tally + 1)
});
This way your code is more reusable

Something like this?
class App extends React.Component {
constructor() {
super();
this.state = {
userList: {
'12': {
name: 'Achilles',
habits: {
Exercise: {
tally: 123
},
'Eat Vegetables': {
tally: 231
}
}
}
}
}
}
inc() {
const {userList} = this.state;
userList['12'].habits['Exercise'].tally++;
this.setState({userList});
}
render() {
return (
<div>
<h2>{this.state.userList['12'].habits['Exercise'].tally}</h2>
<button onClick={() => this.inc()}>Increment</button>
</div>
)
}
};
Play with it here:
https://codesandbox.io/s/10w0o6vn0l

Related

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

Is it possible to set a type that doesn't invoke the render() method in React-Native? mobx state tree

I'm in the process of familiarizing myself with React right now. I chose mobx-state-tree for state management.
Since I used the MVP pattern in my Android projects, I would apply the same principle to mobx-state-tree.
How I currently implemented, it works.
However, I would like to define the ScreenView implementation in the model.
Is there a way to define a model type that does not trigger a render () event?
My desired result:
const AuthScreenModel = types
.model('AuthStore', {
screen: types.enumeration('Screen', ['auth', 'verification', 'name']),
screenView: types.norefresh(ScreenViewInterface),
phoneModel: types.optional(PhoneModel, {})
})
My current Workaround:
const AuthScreenModel = types
.model('AuthStore', {
screen: types.enumeration('Screen', ['auth', 'verification', 'name']),
phoneModel: types.optional(PhoneModel, {})
})
.views((self: any) => {
self.screenView = null
return {
getScreenIndex(): number {
if (self.screen === 'verification')
return 1
if (self.screen === 'name')
return 2
return 0
}
}
})
.actions((self: any) => {
return {
setScreen(screen: string) {
self.screen = screen
},
setScreenIndex(screenIndex: number) {
self.screenIndex = screenIndex
},
setScreenView(screenView: AuthScreenView) {
self.screenView = screenView
},
swipeNext() {
if (self.screenView) {
self.screenView.scrollBy(self.getScreenIndex() < 2 ? 1 : 0)
}
},
swipePrev() {
if (self.screenView) {
self.screenView.scrollBy(self.getScreenIndex() > 0 ? -1 : 0)
}
}
}
})
const AuthScreenStore = AuthScreenModel.create({
screen: 'auth',
phoneModel: PhoneModel.create({
country: CountryModel.create({}),
phoneNumber: ''
})
})
Using this method's of react life cycle you can choose to call render or not.
For Example, you can use shouldComponentUpdate(); to listening for updates, and return a false when you don't want to render the new view.

Apollo seems to refresh, when state is mapped to props, how can i prevent it?

I've build a component which basically list entries in a table, on top of that, i have another component to which filters can be applied. It all works really great with Apollo.
I'm trying to add deep linking into the filters, which on paper seems incredible simple, and i almost had i working.
Let me share some code.
const mapStateToProps = ({ activeObject }) => ({ activeObject });
#withRouter
#connect(mapStateToProps, null)
#graphql(FILTER_REPORT_TASKS_QUERY, {
name: 'filteredTasks',
options: (ownProps) => {
const filters = queryString.parse(location.search, { arrayFormat: 'string' });
return {
variables: {
...filters,
id: ownProps.match.params.reportId,
},
};
},
})
export default class TasksPage extends Component {
static propTypes = {
filteredTasks: PropTypes.object.isRequired,
activeObject: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
const filters = queryString.parse(location.search, { arrayFormat: 'string' });
this.state = { didSearch: false, initialFilters: filters };
this.applyFilter = this.applyFilter.bind(this);
}
applyFilter(values) {
const variables = { id: this.props.match.params.reportId };
variables.searchQuery = values.searchQuery === '' ? null : values.searchQuery;
variables.categoryId = values.categoryId === '0' ? null : values.categoryId;
variables.cardId = values.cardId === '0' ? null : values.cardId;
/*
this.props.history.push({
pathname: `${ this.props.history.location.pathname }`,
search: '',
});
return null;
*/
this.props.filteredTasks.refetch(variables);
this.setState({ didSearch: true });
}
..... Render functions.
Basically it calls the apply filter method, when a filter is chosen.
Which all works great, my problem is that when the activeObject is updated (By selecting a entry in the list). It seems to run my HOC graphql, which will apply the filters from the URL again, ignoring the filters chosen by the user.
I tried to remove the query strings from the url, once filters are applied, but i get some unexpected behavior, basically it's like it doesn't fetch again.
How can i prevent Apollo from fetching, just because the redux pushes new state?
I actually solved this by changing the order of the HOC's.
#graphql(FILTER_REPORT_TASKS_QUERY, {
name: 'filteredTasks',
options: (ownProps) => {
const filters = queryString.parse(location.search, { arrayFormat: 'string' });
return {
variables: {
...filters,
id: ownProps.match.params.reportId,
},
};
},
})
#withRouter
#connect(mapStateToProps, null)

ComponentDidMount() not working

I am making a game using matter.js in reactjs.
I am suffering a problem where I cannot return anything in the render().
Probably this is because matter.js has its own renderer.
So I did not retun anytihng into the render section.
If i don't return anything in the render section componentDidMount do not run.
I am Unable to run componentDidMount() and i cannot call any function even though i bind it.
I am calling a start_game() function to start game but any other function that I call within start_game() an error comes which says that no such function exist even though I have declared it within the class.
import React, { Component } from 'react';
import Matter from 'matter-js';
const World = Matter.World;
const Engine = Matter.Engine;
const Renderer = Matter.Render;
const Bodies = Matter.Bodies;
const Events = Matter.Events;
const Mouse = Matter.Mouse;
const MouseConstraint = Matter.MouseConstraint;
const Body = Matter.Body;
class Walley extends Component
{
constructor(props)
{
super(props);
this.world = World;
this.bodies = Bodies;
this.engine = Engine.create(
{
options:
{
gravity: { x: 0, y: 0,}
}
});
this.renderer = Renderer.create(
{ element: document.getElementById('root'),
engine: this.engine,
options:
{
width: window.innerWidth,
height: window.innerHeight,
background: 'black',
wireframes: false,
}
});
this.state = {
play_game: false,
score:0,
};
this.play_game=this.play_game.bind(this);
}
play_game(){
this.setState
({
score: 50,
});
}
startGame()
{
console.log(this.state.play_game);
//don't know how here the value of play_game is set to true even it was false in componentWillMount()
if(this.state.play_game){
this.play_game();
//unable to access play_game
}
}
componentWillMount()
{
if (confirm("You want to start game?"))
{
this.setState({play_game: true});
}
console.log(this.state.play_game);
//even though play_game is set true above yet output at this console.log remains false
}
render()
{
if(this.state.play_game)
this.startGame();
}
componentDidMount()
{
//unable to access
console.log('Did Mount');
}
}
export default Walley;
render needs to be a pure function, meaning you should not call startGame from inside render. I think you want something closer to this:
// remove this
// componentWillMount() {}
componentDidMount() {
if (confirm("You want to start game?")) {
this.setState({play_game: true}, () => {
console.log(this.state.play_game);
});
startGame();
}
}
render() {
// eventually you will want to return a div where you can put your matter.js output
return null;
}

Redux mutation detected between dispatches

I'm having some trouble with a react redux I'm currently working on. I'm relatively new to Redux so maybe I'm missing a simple concept here but what I'm trying to do is build a deck building app for a card game and I want to be able to save the deck anytime a user adds or removes a card from their deck.
However, anytime I click add or remove I'm receiving the following error message while trying to dispatch an update action.
The error message reads as follows:
Uncaught Error: A state mutation was detected between dispatches, in the path `decks.0.cards.mainboard.0.quantity`. This may cause incorrect behavior.
My container component
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import DeckMobileDisplay from './DeckMobileDisplay';
import * as deckActions from '../../actions/deckActions';
export class DeckEditorContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
deck: Object.assign({}, this.props.deck)
}
this.addCard = this.addCard.bind(this);
this.removeCard = this.removeCard.bind(this);
}
addCard(board, cardName) {
let deck = this.state.deck;
let cards = this.state.deck.cards;
cards[board].forEach(i => {
if(i.name === cardName)
i.quantity += 1;
});
const update = Object.assign(deck, cards);
this.props.deckActions.updateDeck(update).then(deck => {
console.log(deck);
})
.catch(err => {
console.log(err);
});
}
removeCard(board, cardName) {
let deck = this.state.deck;
let cards = this.state.deck.cards;
cards[board].forEach(i => {
if(i.name === cardName) {
if (i.quantity === 1) {
cards[board].splice(cards[board].indexOf(i), 1);
}
else {
i.quantity -= 1;
}
}
});
const update = Object.assign(deck, cards);
this.props.deckActions.updateDeck(update).then(deck => {
console.log(deck);
})
.catch(err => {
console.log(err);
});
}
render() {
const deck = Object.assign({}, this.props.deck);
return (
<div className="editor-container">
<DeckMobileDisplay
deck={deck}
addCard={this.addCard}
removeCard={this.removeCard}
/>
</div>
);
}
}
DeckEditorContainer.PropTypes = {
deck: PropTypes.object
};
function getDeckById(decks, id) {
const deck = decks.filter(deck => deck.id == id);
if (deck.length) return deck[0];
return null;
}
function mapStateToProps(state, ownProps) {
const deckId = ownProps.params.id;
let deck = {
id: '',
userId: '',
cards: []
}
if (state.decks.length > 0) {
deck = getDeckById(state.decks, deckId);
}
return {
deck: deck
};
}
function mapDispatchToProps(dispatch) {
return {
deckActions: bindActionCreators(deckActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(DeckEditorContainer);
Component for DeckMobileDisplay
import React, {PropTypes} from 'react';
import TabContainer from '../common/Tabs/TabContainer';
import Tab from '../common/Tabs/Tab';
import CardSearchContainer from '../CardSearch/CardSearchContainer';
import DeckList from './DeckList.js';
class DeckMobileDisplay extends React.Component {
render() {
return (
<TabContainer>
<Tab title="DeckList">
<DeckList
deck={this.props.deck}
addCard={this.props.addCard}
removeCard={this.props.removeCard}
/>
</Tab>
<Tab title="Search">
<CardSearchContainer
addCard={this.props.addCard}
removeCard={this.props.removeCard}
/>
</Tab>
<Tab title="Stats">
<p>stats coming soon...</p>
</Tab>
</TabContainer>
);
}
}
DeckMobileDisplay.propTypes = {
deck: PropTypes.object.isRequired,
addCard: PropTypes.func.isRequired,
removeCard: PropTypes.func.isRequired
}
export default DeckMobileDisplay;
Related Actions
export function createDeck(deck) {
return dispatch => {
dispatch(beginAjaxCall());
const config = {
method: 'POST',
headers: { 'Content-Type' : 'application/json' },
body : JSON.stringify({deck: deck})
};
return fetch(`http://localhost:3000/users/${deck.userId}/decks`, config)
.then(res => res.json().then(deck => ({deck, res})))
.then(({res, deck}) => {
if (res.status >= 200 && res.status < 300) {
dispatch(createDeckSuccess(deck.deck));
}
else
dispatch(createDeckFailure(deck));
})
.catch(err => {
console.log(err);
dispatch(ajaxCallError(err));
});
};
}
export function updateDeck(deck) {
return dispatch => {
dispatch(beginAjaxCall());
const body = JSON.stringify({deck: deck});
const config = {
method: 'PUT',
headers : { 'Content-Type' : 'application/json' },
body: body
};
return fetch(`http://localhost:3000/decks/${deck.id}`, config)
.then(res => res.json().then(deck => ({deck, res})))
.then(({res, deck}) => {
if (res.status >= 200 && res.status < 300) {
dispatch(updateDeckSuccess(deck.deck));
}
dispatch(ajaxCallError(err));
})
.catch(err => {
console.log(err);
dispatch(ajaxCallError(err));
});
};
}
export function updateDeckSuccess(deck) {
return {
type: types.UPDATE_DECK_SUCCESS,
deck
};
}
And my deck Reducer
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function deckReducer(state = initialState.decks, action) {
switch (action.type) {
case types.LOAD_USERS_DECKS_SUCCESS:
return action.decks;
case types.CREATE_DECK_SUCCESS:
return [
...state,
Object.assign({}, action.deck)
]
case types.UPDATE_DECK_SUCCESS:
return [
...state.filter(deck => deck.id !== action.deck.id),
Object.assign({}, action.deck)
]
default:
return state;
}
}
If you need to see more of the app the repo is here:
https://github.com/dgravelle/magic-redux
Any kind of help would be appreciated, thanks!
Your problem is caused because you are modifying component's state manually.
One Redux's principle is:
State is read-only
The only way to change the state is to emit an action, an object
describing what happened.
This ensures that neither the views nor the network callbacks will
ever write directly to the state. Instead, they express an intent to
transform the state. Because all changes are centralized and happen
one by one in a strict order, there are no subtle race conditions to
watch out for. As actions are just plain objects, they can be logged,
serialized, stored, and later replayed for debugging or testing
purposes.
In the method removeCard you are modifying the state:
removeCard(board, cardName) {
let deck = this.state.deck;
//This is just a reference, not a clone
let cards = this.state.deck.cards;
cards[board].forEach(i => {
if(i.name === cardName) {
if (i.quantity === 1) {
//Here you are modifying cards, which is a pointer to this.state.deck.cards
cards[board].splice(cards[board].indexOf(i), 1);
}
else {
//Here you are modifying cards, which is a pointer to this.state.deck.cards
i.quantity -= 1;
}
}
});
//... more stuff
}
One concept you might be missing is that this.state.deck.cards is a reference/pointer to the Array's memory position. You need to clone it if you want to mutate it.
One solution could be to clone the original array instead:
removeCard(board, cardName) {
let deck = this.state.deck;
//Here you are cloning the original array, so cards references to a totally different memory position
let cards = Object.assign({}, this.state.deck.cards);
cards[board].forEach(i => {
if(i.name === cardName) {
if (i.quantity === 1) {
cards[board].splice(cards[board].indexOf(i), 1);
}
else {
i.quantity -= 1;
}
}
});
//... more stuff
}
Hope it helps you.

Resources