I'm working on a React SPA and trying to render JSON data with a filter. I have several containers that each have a class value. When clicking on a container, I am trying to pass a value to a setState. When stepping out of the map function, however, I cannot get the new state change to update.
I'm assuming that the render state isn't getting updated for the state.
import React, { Component } from "react";
import ListData from "../data/list.json";
class Levels extends Component {
constructor(props) {
super(props);
this.state = { newFilter: "" };
this.onClick = this.setFilter.bind(this);
}
setFilter = e => {
console.log(e); //This returns the correct class.
this.setState({ newFilter: e }); //This will not update
};
render() {
console.log(this.state.newFilter); //This just returns the initial state of ''
const activeTab = this.props.keyNumber;
let levelFilter = ListData.filter(i => {
return i.level === activeTab;
});
let renderLevel = levelFilter.map((detail, i) => {
let short_desc;
if ((detail.short || []).length === 0) {
short_desc = "";
} else {
short_desc = <p>{detail.short}</p>;
}
return (
<div
className={`kr_sItem ${detail.class}`}
data-value={detail.class}
onClick={() => this.onClick(detail.class)}
value={detail.class}
key={i}
>
<h1>{detail.title}</h1>
{short_desc}
<img src={`${detail.img}`} />
</div>
);
});
console.log(this.state.newFilter);
return (
<div id="kr_app_wrapper">
<div id="lOneWrapper">{renderLevel}</div>
<div id="lTwoWrapper"></div>
</div>
);
}
}
export default Levels;
Here is the code calling in the Component:
import React, {Component} from 'react';
import Levels from './components/levels2';
import {
Route,
NavLink,
HashRouter
} from "react-router-dom";
import LevelTwo from "./levelTwo";
class LevelOne extends Component{
render(){
return(
<div id="lOneWrapper">
<NavLink to='/Level2'><Levels keyNumber={1} /></NavLink>
</div>
)
}
}
export default LevelOne;
Edit: As pointed out in the comments, binding an arrow function is pointless but it wouldn't cause the error here, so there must be something else going on in some other part of your code.
The problem is you are trying to bind an arrow function that doesn't have an implicit this. Either call setFilter directly (no need to bind this with arrow functions) or change your setFilter function to a regular function:
class Levels extends Component{
constructor(props){
super(props);
this.state = {newFilter: ''};
this.onClick = this.setFilter.bind(this);
}
setFilter(e) {
console.log(e); //This returns the correct class.
this.setState({newFilter: e}); //This will not update
}
I'm having serious issues with the "new" React Context ( https://reactjs.org/docs/context.html ) to work like I want/expect from the documentation. I'm using React v.16.8.6 (upgrading will probably take ages, it's a big app). I know there is a bit of a mix between old and new stuff but plz don't get stuck on that..
I did it like this to be as flexible as possible but it doesn't work.
The issue is, when it comes to contextAddToCart(..) it only executes the empty function instead of the one I defined in state as the documentation this.addToCart. I have consumers in other places as well. It seems like perhaps it's executing this in the wrong order. Or every time a Compontent imports MinicartContext it's reset to empty fn.. I don't know how to get around this..
I'll just post the relevant code I think will explain it best:
webpack.config.js:
const APP_DIR = path.resolve(__dirname, 'src/');
module.exports = function config(env, argv = {}) {
return {
resolve: {
extensions: ['.js', '.jsx'],
modules: [
path.resolve(__dirname, 'src/'),
'node_modules',
],
alias: {
contexts: path.resolve(__dirname, './src/contexts.js'),
},
contexts.js
import React from 'react';
export const MinicartContext = React.createContext({
addToCart: () => {},
getState: () => {},
});
MinicartContainer.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
MinicartContext,
} from 'contexts';
export default class MinicartContainer extends Component {
constructor(props) {
super(props);
this.addToCart = (product, qty) => {
const { prices } = product;
const { grandTotal, qtyTotal } = this.state;
this.setState({
grandTotal: grandTotal + prices.price,
qtyTotal: qtyTotal + qty,
});
};
this.state = {
grandTotal: -1,
qtyTotal: -1,
currencyCode: '',
addToCart: this.addToCart,
};
}
render() {
const { children } = this.props;
return (
<MinicartContext.Provider value={this.state}>
{children}
</MinicartContext.Provider>
);
}
Header.jsx:
import React, { Component } from 'react';
import {
MinicartContext,
} from 'contexts';
class Header extends Component {
render() {
return (
<div>
<MinicartContainer MinicartContext={MinicartContext}>
<Minicart MinicartContext={MinicartContext} />
</MinicartContainer MinicartContext={MinicartContext}>
{/* stuff */}
<MinicartContainer MinicartContext={MinicartContext}>
<Minicart MinicartContext={MinicartContext} />
</MinicartContainer MinicartContext={MinicartContext}>
</div>
)
}
}
export default Header;
AddToCartButton.jsx
import {
MinicartContext,
} from 'contexts';
export default class AddToCartButton extends Component {
addToCart(e, contextAddToCart) {
e.preventDefault();
const QTY = 1;
const { product, active } = this.props;
// doing stuff ...
contextAddToCart(product, QTY);
}
render() {
return (
<React.Fragment>
<MinicartContext.Consumer>
{({context, addToCart}) => (
<div
onClick={(e) => { this.addToCart(e, addToCart); }}
Seems to me that you don't have fully understand how the context API words.
Here's my HOC implementation of contexts, maybe it can help you to understand better how things work.
export const MinicartContext = React.createContext({}) // Export the Context so we can use the Consumer in class and functional components (above). Don't use the Provider from here.
// Wrap the provider to add some custom values.
export const MinicartProvider = props => {
const addToCart = () => {
//Add a default version here
};
const getState = () => {
//Add a default version here
};
// Get the custom values and override with instance ones.
const value = {addToCart, getState, ...props.value}
return <MinicartContext.Provider value={value}>
{props.children}
</MinicartContext.Provider>
}
Then when using the provider:
const SomeComponent = props => {
const addToCart = () => {
//A custom version used only in this component, that need to override the default one
};
//Use the Wrapper, forget the MinicartContext.Provider
return <MinicartProvider value={{addToCart}}>
/* Stuff */
</MinicartProvider>
}
And when using the consumer you have three options:
Class Components with single context
export default class AddToCartButton extends Component {
static contextType = MinicartContext;
render (){
const {addToCart, getState} = this.context;
return (/*Something*/)
}
}
Class Components with multiple contexts
export default class AddToCartButton extends Component {
render (){
return (
<MinicartContext.Consumer>{value => {
const {addToCart, getState} = value
return (/*Something*/)
}}</MinicartContext.Consumer>
)
}
}
Functional Components
const AddToCartButton = props => {
const {addToCart, getState} = useContext(MinicartContext);
}
You can create the Wrapper Provider as a class component too, and pass the full state as value, but it's unnecessary complexity.
I Recommend you take a look at this guide about contexts, and also, avoid using the same name on the same scope... Your AddToCartButton.jsx file was reeeeally confusing :P
The issue I had was that I was using <MinicartContainer> in multiple places but all should act as one and the same. Changing it so it wrapped all elements made other elements reset their state when the context updated.
So the only solution I found was to make everything static (including state) inside MinicartContainer, and keep track of all the instances and then use forceUpdate() on all (needed) instances. (Since I am never doing this.setState nothing ever updates otherwise)
I though the new React context would be a clean replacement for things like Redux but as it stands today it's more a really vague specification which can replace Redux in a (sometimes) non standard way.
If you can just wrap all child Consumers with a single Provider component without any side-effects then you can make it a more clean implementation. That said I don't think what I have done is bad in any way but not what people expect a clean implementation should look like. Also this approach isn't mentioned in the docs at all either.
In addition to Toug's answer, I would memoize the exposed value prop of the provider. Otherwise it will re-render it's subscribers every time even if the state doesn't change.
export const MinicartContext = React.createContext({}) // Export the Context so we can use the Consumer in class and functional components (above). Don't use the Provider from here.
// Wrap the provider to add some custom values.
export const MinicartProvider = props => {
const addToCart = () => {
//Add a default version here
};
const getState = () => {
//Add a default version here
};
// Get the custom values and override with instance ones.
const value = useMemo(
() => ({addToCart, getState, ...props.value}),
[addToCart, getState, props.value]
);
return <MinicartContext.Provider value={value}>
{props.children}
</MinicartContext.Provider>
}
I need to make a new api request to fetch data for a given dataId.
this value lives in the Context.
import { MyContext } from './Context'
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
dataId: this.context.state.dataId // tried setting state first but didn´t work.
}
this.details = this.details.bind(this)
}
details() {
fetch('https://api.mydomain.com/' + this.context.state.dataId)
.then(response => response.json())
.then(data => this.setState({ data: data }));
}
componentDidMount() {
this.details()
}
render() {
return(
<MyContext.Consumer>
{(context) => (
<div>data: {JSON.stringify(data)} dataId: {context.state.dataId}</div>
)}
</MyContext.Consumer>
)
}
}
MyComponent.contextType = MyContext;
export default MyComponent
from others components I can set new values like
this.context.setDataId(1)
and this will show up correctly but the problem is that is not making a new fetch to get new data for the dataId that changed in the Context.
not sure what´s the correct lifecycle method I can use to detect changes in the context and make a new call to this.details()
I didn´t add the Context code here because it works fine. but if you need to see it please let me know.
In react, you must use life cycle hooks to inspect data such as props or context, to know if the state needs to update for your component. The most common life cycle hook for this purpose is componentDidUpdate(). it gives you the ability to decide whether or not your component needs to update state/rerender based on changes in props that caused the component to update. the following should work for your use case:
import { MyContext } from './Context'
class MyComponent extends Component {
state = {
data:[],
dataId:null
}
details = () => {
// we only want to update if dataId actually changed.
if(this.context.state.dataId !== this.state.dataId){
this.setState({dataId:this.context.state.dataId});
fetch('https://api.mydomain.com/' + this.context.state.dataId)
.then(response => response.json())
.then(data => this.setState({ data: data }));
}
}
componentDidMount() {
this.details()
}
componentDidUpdate() {
this.details();
}
render() {
return(
<MyContext.Consumer>
{(context) => (
<div>data: {JSON.stringify(this.state.data)} dataId: {context.state.dataId}</div>
)}
</MyContext.Consumer>
)
}
}
MyComponent.contextType = MyContext;
export default MyComponent;
I'm trying to transfer data from parent RN Component to child.
My parent RN Component:
export default class ListOfUserPhotos extends Component {
constructor(props) {
super(props);
this.state = {
...
};
}
componentDidMount() {
...
}
render() {
return this.state.photosKeysArray.map((photo, index) => {
console.warn('photoData - ' + photo)
// returns 'photoData - {photoValue}' - everything is OK here
return <ListOfUserPhotos_ListView key = {index}
description = {photo.description}
...
/>
})
}
}
};
My child RN Component:
export default class ListOfUserPhotos_ListView extends Component {
render() {
return(
<View style = {listOfUserPhotosStyle.container_list}>
{console.warn('desc = ' + this.props.description)}
// returns 'desc = undefined' - everything is BAD here
...
</View>
)
}
}
Before passing data to the child component I can console that data and see it there. But inside the component the props are undefined.
Can someone explain me what I did wrong? Or transferring data between RN Component should be implemented in other way?
As you are retrieving the data asynchronously you should do the following to ensure that the component gets re-rendered.
componentDidMount() {
// You need to set the state here to cause a re-render
this.setState({
photosKeysArray: firebase.response
});
}
render() {
const { photosKeysArray } = this.state;
if(photosKeysArray.length === 0) {
return <Text>Loading...</Text>
}
// this will be returned if above condition is not met
return photosKeysArray.map((photo, index) => {
return (
<ListOfUserPhotos_ListView
key = {index}
description = {photo.description}
/>
);
})
};
I have a React component that's created dynamically, with data loaded into its state. This state is being cleared between click events, resulting in the visible info being lost (undefined). Where has it gone?
const React = require('react');
const ReactDOM = require('react-dom');
class App extends React.Component {
constructor(props) {
super(props);
this.nodesQuantity = 0;
this.state = {roots: []};
}
nextId(me) {
return me.nodesQuantity++;
}
componentDidMount() {
var roots = [1,2,3].map(node => {
let nextId = this.nextId(this);
return <Node key={nextId} data={node} nextId={this.nextId} depth={0} god={this} />
});
this.setState({roots: roots});
}
render() {
return (
<ul>{this.state.roots}</ul>
)
}
}
class Node extends React.Component{
constructor(props) {
super(props);
this.state = {data: this.props.data, childNodes: [], visible: true};
this.toggleExpanded = this.toggleExpanded.bind(this);
}
toggleExpanded(event) {
console.log(this.state.childNodes.length)
let val={id:"asdf",label:"asdf"}
if (this.state.childNodes.length > 0) {
this.state.childNodes.forEach((childNode) => {
childNode.setState({visible: !childNode.state.visible})
});
} else {
this.setState((oldState, props) => {
let nextId = props.nextId(props.god);
console.log(nextId);
oldState.childNodes.push(<Node key={nextId} data={val} nextId={props.nextId} depth={props.depth+1} god={props.god} />);
});
}
event.stopPropagation();
}
render() {
return (
<li onClick={this.toggleExpanded}>
{this.state.data.id} ({this.state.data.label})
<ul>{this.state.childNodes}</ul>
</li>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
(oldState, props) => {
let nextId = props.nextId(props.god);
console.log(nextId);
oldState.childNodes.push(<Node key={nextId} data={val} nextId={props.nextId} depth={props.depth+1} god={props.god} />);
}
doesn't return anything... possibly the source of your undefined. You're also modifying previous state via push which is forbidden in the react docs:
prevState is a reference to the previous state. It should not be
directly mutated. Instead, changes should be represented by building a
new object based on the input from prevState and props.
You ought to be doing something like:
return {childNodes:[...(oldState.childNodes)]}
I solved it. My main problem was a misunderstanding of react lifecycle.
This answer, albeit a bit outdated, was quite useful in understanding my problem.
Also a bit switch was to store the Node as json and only build the nodes during render step. Updates are triggered with setState which pass through the componentWillReceiveProps method that updates the state of the node json and proceeds to render
Here's my solution: https://jsfiddle.net/n3ygz7uk/2/