I have a main component with a child chart component. On connecting to a websocket, the main component updates the state of the child chart component. This however does not redraw. When I click on the chart however, the labels appear, and when I click again, the values appear with the labels.
Main.js:
import IO from 'socket.io-client';
import React from "react";
import { Switch, Route } from 'react-router-dom';
import { Chart } from "./Chart";
let ftse100Tickers = require('./ftse_100_tickers.json');
let randomInt = Math.floor(Math.random() * ftse100Tickers.tickers.length);
/**
* Main application component which contains
*/
export class Main extends React.Component {
componentWillMount() {
this.socket = IO(location.protocol + "//" + document.domain + ":" + location.port);
this.socket.on("connect", (() => this.connect()));
this.socket.on("disconnect", (() => this.disconnect()));
this.socket.on("initial data", ((data) => this.createInitialChart(data)))
}
connect(){
this.setState({status: 'connected'});
this.socket.emit("get initial data", this.state.ticker);
}
disconnect(){
this.setState({status: 'disconnected'})
}
createInitialChart(data){
let tempErrorChart= this.state.errorChart;
for (let row of data){
tempErrorChart.labels.push(row.time_stamp);
tempErrorChart.datasets[0].data.push(row.error);
}
this.setState({errorChart: tempErrorChart});
}
constructor(props){
super(props);
this.state = {
errorChart: {
labels: [],
datasets: [
{
label: 'Error',
data: [],
},
]
},
status: 'disconnected',
ticker : ftse100Tickers.tickers[randomInt],
twitter : ftse100Tickers.twitter[randomInt]
}
}
render() {
return (
<div className="row">
<div className="row">
<div className="col-lg-6">
<div className="card border-0">
<div className="card-body">
<Chart chart={this.state.errorChart}/>
</div>
</div>
</div>
</div>
</div>
)
}
}
The chart component is as so:
Chart.js
import { Line } from "react-chartjs-2"
import React from "react";
/*
* General charting component used for rendering charts
*/
export class Chart extends React.Component {
render() {
return (
<Line data={this.props.chart} options={{}}/>
)
}
}
I see one problem and that is you are not changing object references in this.state.errorChart upon errorChart update before you call setState. Even though you push to its properties new items, the object and even the inner array references do not change, and if the Line component does some props checking on whether it should rerender itself or not, it figures by receiving still the same references that nothing has changed and there is no need to rerender.
Now this was just my assumption, but either way it is a good practice to always create new objects while creating new state once those objects are about to be modified. This allows for fast object (state) references comparisons in shouldComponentUpdate methods or while using PureComponentwhich in turn makes it easier and more performant to determine whether to rerender the component or not. On the other hand, if you would use the same references still, you would have to implement deep comparison of the old and the new state, which is definitely more expensive and very fragile in the long run.
Example on how to correctly update the state follows:
createInitialChart(data) {
const errorChart = this.state.errorChart
const newErrorChart = {
...errorChart
}
newErrorChart.labels = [...errorChart.labels, data.map(row => row.time_stamp)]
newErrorChart.datasets[0].data = [
...errorChart.datasets[0].data,
data.map(row => row.error)
]
this.setState({ errorChart: newErrorChart })
}
Edit:
By looking at the component's shouldComponentUpdate implementation - ChartComponent, It can be clearly seen, that there are multiple options on how to make the Line rerender, eg. by giving redraw={true} prop to the Line component. The procedure above is generally still the safest way to ensure rerender though.
You might need componentWillReceiveProps(nextProps, nextState).
You can compare the old state here with the new state and update the state accordingly.
Please set the initialState like so:
constructor(props){
super(props);
this.state = {errorChart: {...}}; //your initial values here.
}
then,
componentWillReceiveProps(nextProps, nextState){
if(this.state.errorChart !== nextState.errorChart){
let tempErrorChart = {...this.state.errorChart};
for (let row of data){
tempErrorChart.labels.push(row.time_stamp);
tempErrorChart.datasets[0].data.push(row.error);
}
this.setState({errorChart: tempErrorChart});
}
}
Related
I have a very simple component with a text field and a button:
It takes a list as input and allows the user to cycle through the list.
The component has the following code:
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
This component works great, except I have not handled the case when the state changes. When the
state changes, I would like to reset the currentNameIndex to zero.
What is the best way to do this?
Options I have conciderred:
Using componentDidUpdate
This solution is ackward, because componentDidUpdate runs after render, so I need to add a clause
in the render method to "do nothing" while the component is in an invalid state, if I am not careful,
I can cause a null-pointer-exception.
I have included an implementation of this below.
Using getDerivedStateFromProps
The getDerivedStateFromProps method is static and the signature only gives you access to the
current state and next props. This is a problem because you cannot tell if the props have changed. As
a result, this forces you to copy the props into the state so that you can check if they are the same.
Making the component "fully controlled"
I don't want to do this. This component should privately own what the currently selected index is.
Making the component "fully uncontrolled with a key"
I am considering this approach, but don't like how it causes the parent to need to understand the
implementation details of the child.
Link
Misc
I have spent a great deal of time reading You Probably Don't Need Derived State
but am largely unhappy with the solutions proposed there.
I know that variations of this question have been asked multiple times, but I don't feel like any of the answers weigh the possible solutions. Some examples of duplicates:
How to reset state in a component on prop change
Update component state when props change
Updating state on props change in React Form
Appendix
Solution using componetDidUpdate (see description above)
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
if(this.state.currentNameIndex >= this.props.names.length){
return "Cannot render the component - after compoonentDidUpdate runs, everything will be fixed"
}
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if(prevProps.names !== this.props.names){
this.setState({
currentNameIndex: 0
})
}
}
}
Solution using getDerivedStateFromProps:
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
copyOfProps?: Props
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
if( state.copyOfProps && props.names !== state.copyOfProps.names){
return {
currentNameIndex: 0,
copyOfProps: props
}
}
return {
copyOfProps: props
}
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
As i said in the comments, i'm not a fan of these solutions.
Components should not care what the parent is doing or what is the current state of the parent, they should simply take in props and output some JSX, this way they are truly reusable, composable and isolated which also makes testing a lot easier.
We can make the NamesCarousel component hold the names of the carousel together with the functionality of the carousel and the current visible name and make a Name component which does only one thing, display the name that comes in through props
To reset the selectedIndex when the items are changing add a useEffect with items as a dependency, although if you just add items to the end of the array you can ignore this part
const Name = ({ name }) => <span>{name.toUpperCase()}</span>;
const NamesCarousel = ({ names }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [names])// when names changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
<Name name={names[selectedIndex]} />
<button onClick={next} disabled={selectedIndex === names.length - 1}>
Next
</button>
</div>
);
};
Now this is fine but is the NamesCarousel reusable? no, the Name component is but the Carousel is coupled with the Name component.
So what can we do to make it truly reusable and see the benefits of designing component in isolation?
We can take advantage of the render props pattern.
Lets make a generic Carousel component which will take a generic list of items and invoke the children function passing in the selected item
const Carousel = ({ items, children }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [items])// when items changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
{children(items[selectedIndex])}
<button onClick={next} disabled={selectedIndex === items.length - 1}>
Next
</button>
</div>
);
};
Now what this pattern actually gives us?
It gives us the ability to render the Carousel component like this
// items can be an array of any shape you like
// and the children of the component will be a function
// that will return the select item
<Carousel items={["Hi", "There", "Buddy"]}>
{name => <Name name={name} />} // You can render any component here
</Carousel>
Now they are both isolated and truly reusable, you can pass items as an array of images, videos, or even users.
You can take it further and give the carousel the number of items you want to display as props and invoke the child function with an array of items
return (
<div>
{children(items.slice(selectedIndex, selectedIndex + props.numOfItems))}
</div>
)
// And now you will get an array of 2 names when you render the component
<Carousel items={["Hi", "There", "Buddy"]} numOfItems={2}>
{names => names.map(name => <Name key={name} name={name} />)}
</Carousel>
Can you use a functional component? Might simplify things a bit.
import React, { useState, useEffect } from "react";
import { Button } from "#material-ui/core";
interface Props {
names: string[];
}
export const NameCarousel: React.FC<Props> = ({ names }) => {
const [currentNameIndex, setCurrentNameIndex] = useState(0);
const name = names[currentNameIndex].toUpperCase();
useEffect(() => {
setCurrentNameIndex(0);
}, names);
const handleButtonClick = () => {
setCurrentIndex((currentNameIndex + 1) % names.length);
}
return (
<div>
{name}
<Button onClick={handleButtonClick}>Next</Button>
</div>
)
};
useEffect is similar to componentDidUpdate where it will take an array of dependencies (state and prop variables) as the second argument. When those variables change, the function in the first argument is executed. Simple as that. You can do additional logic checks inside of the function body to set variables (e.g., setCurrentNameIndex).
Just be careful if you have a dependency in the second argument that gets changed inside the function, then you will have infinite rerenders.
Check out the useEffect docs, but you'll probably never want to use a class component again after getting used to hooks.
You ask what is the best option, the best option is to make it a Controlled component.
The component is too low in the hierarchy to know how to handle it's properties changing - what if the list changed but only slightly (perhaps adding a new name) - the calling component might want to keep the original position.
In all cases I can think about we are better off if the parent component can decide how the component should behave when provided a new list.
It's also likely that such a component is part of a bigger whole and needs to pass the current selection to it's parent - perhaps as part of a form.
If you are really adamant on not making it a controlled component, there are other options:
Instead of an index you can keep the entire name (or an id component) in the state - and if that name no longer exists in the names list, return the first in the list. This is a slightly different behavior than your original requirements and might be a performance issue for a really really really long list, but it's very clean.
If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.
The "canonical" way to do it with classes seems to be getDerivedStateFromProps - and yes that means keeping a reference to the name list in the state and comparing it. It can look a bit better if you write it something like this:
static getDerivedStateFromProps(props: Props, state: State = {}): Partial<State> {
if( state.names !== props.names){
return {
currentNameIndex: 0,
names: props.names
}
}
return null; // you can return null to signify no change.
}
(you should probably use state.names in the render method as well if you choose this route)
But really - controlled component is the way to go, you'll probably do it sooner or later anyway when demands change and the parent needs to know the selected item.
So I am creating my own site for my resume and have run up against an issue I'm not exactly sure how to solve. I am using React and Redux, the issue comes up that what I want is to display small little projects in a carousel type format using react with proper redux integrated in. Currently my file structure looks like:
src/
actions/
index.js
components/
App.js
NavBar.js
Projects.js
projects/
Project1/
Project2/
containers/
Main.js
reducers/
index.js
projects.js
I am not sure exactly how to accomplish what I want, I have searched for a good solution, but haven't really come across anything yet. I am still relatively new to react. What I don't want is to display one project, scroll down display the next, scroll some more... What I have tried is:
let components = [{TicTacToe}, {}];
let index = 0;
export default class Projects extends React.Component {
constructor(props) {
super(props);
this.state = {
component: components[0]
};
}
renderProject(i)
{
this.setState({component: components[i]});
}
backRotate()
{
index--;
if(index < 0)
{
index = components.length - 1;
}
this.renderProject(index);
}
forwardRotate()
{
index++;
if(index >= components.length)
{
index = 0;
}
this.renderProject(index);
}
render() {
return(
<div>
<button onClick='backRotate'>Back</button>
<div class='carousel-container'>
<this.state.component />
</div>
<button onClick='forwardRotate'>Next</button>
</div>
) }
}
I originally thought this would work, but it does break. I am currently running through App.js in my components folder adding the NavBar component and then adding the Main.js container. The reason for this container was the fact that I need a back and forward button to rotate through each project just like a carousel of images. I have considered adding all components and then just hiding and revealing, but this seems like a needless waste of resources and that there should be a better way to accomplish this.
Is there a way to replace a component on a button click? Completely remove the original component and add the new component in? If so, how do I also accomplish this using redux? Currently my projects reducer is just a place holder.
First of all, it breaks because the componentsarray contains objects, and React can't have that.
A valid React element is either a node (HTMLElement, React Component, text or number) or an array of nodes.
If you just want to keep React components in a collection and be able to select which is displayed, first you have to remove the braces in the components array, like so:
let components = [TicTacToe, AnotherComponent];
And you will also need to fix your backRotateand forwardRotate handlers. They can't be string like in html (note the braces instead of quotes):
<button onClick={this.backRotate}>Back</button>
<button onClick={this.forwardRotate}>Next</button>
...and must be bound to your component instance. A way to bind them is to use the Function.prototype.bind method. It should be done in the constructor:
constructor(props) {
super(props);
this.backRotate = this.backRotate.bind(this);
this.forwardRotate= this.forwardRotate.bind(this);
}
Without binding this would not be a reference to your component instance and calling this within the handlers would definitely fail.
TicTacToe and AnotherComponent must be classes extending React.Component or React.PureComponent, or must be pure functional components of props.
Also, its clearer and may be more supported to declare a capitalized variable containing the component:
render() {
const Comp = this.state.component;
return (
<div>
<button onClick={this.backRotate}>Back</button>
<div class='carousel-container'>
<Comp />
</div>
<button onClick={this.forwardRotate}>Next</button>
</div>
);
}
Finally, you should make index a state of your component (it's changed with the controls of the component) and components a prop (it does not change within the component).
Instead of setting the current component in state you will set the current component index:
import React from 'react':
import PropTypes from 'prop-types';
import { TicTacToe, AnotherComponent } from 'path/to/named/imports/TicTacToeAndAnotherComponent';
export default class Projects extends React.Component {
static propTypes = {
components: PropTypes.any,
}
static defaultProps = {
components: [TicTacToe, AnotherComponent],
}
constructor(props) {
super(props);
this.state = {
index: 0,
};
this.backRotate = this.backRotate.bind(this);
this.forwardRotate= this.forwardRotate.bind(this);
}
backRotate() {
const { components } = this.props;
const { index } = this.state;
if(index - 1 < 0) {
this.setState(() => ({ index: components.length - 1 }));
} else {
this.setState(() => ({ index: index - 1 }));
}
}
forwardRotate() {
const { components } = this.props;
const { index } = this.state;
if(index >= components.length) {
this.setState(() => ({ index: 0 }));
} else {
this.setState(() => ({ index: index + 1 }));
}
}
render() {
const Comp = this.props.components[this.state.index];
return (
<div>
<button onClick={this.backRotate}>Back</button>
<div class='carousel-container'>
<Comp />
</div>
<button onClick={this.forwardRotate}>Next</button>
</div>
);
}
}
Either way Redux won't help you with that.
The purpose of Redux is mainly to provide a single source of data (single source of truth), provide actions and reducers to mutate this data, and notify components registered to listen to store updates. Weither you connect your carousel to the store or not isn't really relevant to resolve your iisue.
I'm currently learning react and redux and stumbled into a problem i can't really get my head around. Trying to implement the same functionality
as in this article: https://medium.com/#yaoxiao1222/implementing-search-filter-a-list-on-redux-react-bb5de8d0a3ad but even though the data request from the rest api i'm working with is successfull i can't assign the local state in my component to my redux-state, in order to be able to filter my results. Heres my component:
import React from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as fetchActions from '../../actions/fetchActions'
import Stafflist from './Stafflist'
class AboutPage extends React.Component {
constructor(props) {
super(props)
this.state = {
search: '',
currentlyDisplayed: this.props.store.posts
}
this.updateSearch = this.updateSearch.bind(this)
}
updateSearch(event) {
let newlyDisplayed = this.state.currentlyDisplayed.filter(
(post) => {
return (
post.name.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1
|| post.role.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1
)}
)
console.log(newlyDisplayed)
this.setState({
search: event.target.value.substr(0, 20),
currentlyDisplayed: newlyDisplayed
})
}
render() {
return (
<div className="about-page">
<h1>About</h1>
<input type="text"
value={this.state.search}
onChange={this.updateSearch}
/>
//component for rendering my list of posts.
<Stafflist posts={this.state.currentlyDisplayed} />
</div>
)
}
}
// this is here i assign my api data to this.props.store.posts
function mapStateToProps(state, ownProps) {
return {
store: state
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(fetchActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AboutPage)
Comparing how i assign my stores state to my local component with how it works in the article, it seems to be done in the same way. Mine:
this.state = {
search: '',
currentlyDisplayed: this.props.store.posts
}
article:
this.state = {
searchTerm: '',
currentlyDisplayed: this.props.people
}
within react devtools i can see my data in as it should be in the store, but it won't work to assign it to my local state within the component in order to perform the filtering, and i don't really know how to debug this. My state in the local component just says
State
currentlyDisplayed: Array[0]
Empty array
also if i change
<Stafflist posts={this.state.currentlyDisplayed} />
to
<Stafflist posts={this.props.store.posts} />
the list renders as it should :)
Reducer:
import * as types from '../actions/actionTypes'
import initialState from './initialState'
export default function postReducer(state = initialState.posts, action) {
switch(action.type) {
case types.FETCH_POSTS_SUCCESS:
return action.posts.data.map(post => {
return {
id: post.id,
name: post.acf.name,
role: post.acf.role
}
})
default:
return state
}
}
Any ideas?
The problem with your code is that you do not handle how to get newly received props to your state. This means that when you receive the data from your api call only the props are updated while component state is not.
If you look carefully at the referenced article, in the onInputChange method they recalculate the state from the props whereas your updateState method only filters from the local state.
Setting the state in the constructor only ensures that the props are copied on the initial mount of the component. At that point in time the store only contains the initial state (initialState.posts in your reducer code). This is the price of keeping both component state and store; you must think of how to keep the two in sync after the initial load.
One solution is to update the state in componentWillReceiveProps:
componentWillReceiveProps(nextProps){
const nextFiltered = nextProps.store.posts.filter(your filtering code here);
this.setState({currentlyDisplayed: nextFiltered});
}
This will update your state whenever the component receives new props. Note react has phased out componentWillReceiveProps and you should use getDerivedStateToProps as of react 16.3. Refer here for more details.
I am having a bit of an issue rendering components before the state is set to the data from a returned asynchronous API request. I have a fetch() method that fires off, returns data from an API, and then sets the state to this data. Here is that block of code that handles this:
class App extends Component {
constructor() {
super();
this.state = {
currentPrice: null,
};
}
componentDidMount() {
const getCurrentPrice = () => {
const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
fetch(url).then(data => data.json())
.then(currentPrice => {
this.setState = ({
currentPrice: currentPrice.bpi.USD.rate
})
console.log('API CALL', currentPrice.bpi.USD.rate);
}).catch((error) => {
console.log(error);
})
}
getCurrentPrice();
}
You will notice the console.log('API CALL', currentPrice.bpi.USD.rate) that I use to check if the API data is being returned, and it absolutely is. currentPrice.bpi.USD.rate returns an integer (2345.55 for example) right in the console as expected.
Great, so then I assumed that
this.setState = ({ currentPrice: currentPrice.bpi.USD.rate }) should set the state without an issue, since this data was received back successfully.
So I now render the components like so:
render() {
return (
<div>
<NavigationBar />
<PriceOverview data={this.state.currentPrice}/>
</div>
);
}
}
export default App;
With this, I was expecting to be able to access this data in my PriceOverview.js component like so: this.props.data
I have used console.log() to check this.props.data inside my PriceOverview.js component, and I am getting 'null' back as that is the default I set intially. The issue I am having is that the components render before the API fetch has ran it's course and updated the state with the returned data. So when App.js renders the PriceOverview.js component, it only passes currentPrice: null to it, because the asynchronous fetch() has not returned the data prior to rendering.
My confusion lies with this.setState. I have read that React will call render any time this.setState is called. So in my mind, once the fetch() request comes back, it calls this.setState and changes the state to the returned data. This in turn should cause a re-render and the new state data should be available. I would be lying if I didn't say I was confused here. I was assuming that once the fetch() returned, it would update the state with the requested data, and then that would trigger a re-render.
There has to be something obvious that I am missing here, but my inexperience leaves me alone.. cold.. in the dark throws of despair. I don't have an issue working with 'hard coded' data, as I can pass that around just fine without worry of when it returns. For example, if I set the state in App.js to this.state = { currentPrice: [254.55] }, then I can access it in PriceOverview.js via this.props.data with zero issue. It's the async API request that is getting me here, and I am afraid it has gotten the best of me tonight.
Here App.js in full:
import React, { Component } from 'react';
import './components/css/App.css';
import NavigationBar from './components/NavigationBar';
import PriceOverview from './components/PriceOverview';
class App extends Component {
constructor() {
super();
this.state = {
currentPrice: null,
};
}
componentDidMount() {
const getCurrentPrice = () => {
const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
fetch(url).then(data => data.json())
.then(currentPrice => {
this.setState = ({
currentPrice: currentPrice.bpi.USD.rate
})
console.log('API CALL', currentPrice.bpi);
}).catch((error) => {
console.log(error);
})
}
getCurrentPrice();
}
render() {
return (
<div>
<NavigationBar />
<PriceOverview data={this.state.currentPrice}/>
</div>
);
}
}
export default App;
Here is PriceOverview.js in full:
import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
class PriceOverview extends Component {
constructor(props) {
super(props);
this.state = {
currentPrice: this.props.data
}
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.state.currentPrice != null ? <div className="price">{this.state.currentPrice}</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
export default PriceOverview;
Thank you in advance to any help, it's much appreciated.
this.setState ({
currentPrice: currentPrice.bpi.USD.rate
})
Do not put an = in this.setState
Ok First thing, when you're writting code on React the components that hold state are the class base components so ... What I see here is that you're creating two class base components so when you pass down props from your app class component to your PriceOverview wich is another class base component you're essentially doing nothing... Because when your constructor on your PriceOverview get call you're creating a new state on that Component and the previous state ( that's is the one you want to pass down) is being overwritten and that's why you're seem null when you want to display it. So it should work if you just change your PriveOverview component to a function base component ( or a dumb component). So this way when you pass down the state via props, you're displaying the correct state inside of your div. This is how would look like.
import React from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
const PriceOverview = (data) => {
return (
<div className="overviewBar">
<div className="currentPrice panel">
//Im calling data here because that's the name you gave it as ref
//No need to use 'this.props' you only use that to pass down props
{data != null ? <div className="price">
{data}</div> : <div className="price">Loading...</div>
}
</div>
</div>
)
}
}
export default PriceOverview;
Whenever you're writing new components start always with function base components if you component is just returning markup in it and you need to pass some data go to his parent component update it (making the api calls there or setting the state there) and pass down the props you want to render via ref. Read the React docs as much as you can, hope this explanation was useful (my apologies in advance if you don't understand quite well 'cause of my grammar I've to work on that)
The thing is constructor of any JS class is called only once. It is the render method that is called whenever you call this.setState.
So basically you are setting currentPrice to null for once and all in constructor and then accessing it using state so it will always be null.
Better approch would be using props.
You can do something like this in your PriceOverview.js.
import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';
class PriceOverview extends Component {
constructor(props) {
super(props);
this.state = {
}
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.props.data!= null ? <div className="price">{this.props.data}</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
export default PriceOverview;
Or you can use react lifecycle method componentWillReceiveProps to update the state of PriceOverview.js
componentWillReceiveProps(nextProps) {
this.setState({
currentPrice:nextProps.data
});
}
render() {
return (
<div className="overviewBar">
<div className="currentPrice panel">
{ this.state.currentPrice != null ? <div className="price">{this.state.currentPrice }</div> : <div className="price">Loading...</div> }
</div>
</div>
)
}
}
I have looked at other questions that seemingly had a similar issue, but none of the accepted answers have solved my issue. I am attempting to fetch new names and load them into child component when redux is updated with new IDs.
When I use only redux and no state (as I would prefer), the new IDs do not get passed along to the child component and the names do not load at all
Alternatively, I have tried using state for the names in the child component (as you can see in the commented text below). However ... Oddly enough, every time the IDs are changed, the component loads the names based on the previous IDs rather than the current IDs.
Redux
const initialState = {
objectOfIds: {"someID":"someID", "aDifferentID":"aDifferentID"}, // I know this format may seem redundant and odd, but I have to keep it this way
arrayOfNames: ["John Doe", "Jenny Smith"]
}
Parent Compoenent
// React
import React from 'react';
import firebase from 'firebase';
// Components
import ListOfNames from './ListOfNames';
// Redux
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {set} from './../actions/index.js';
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.changeIDs = this.changeIDs.bind(this);
}
changeIDs() {
this.props.set("objectOfIds",{"aNewID":"aNewID","someOtherID":"someOtherID","anotherID":"anotherID"});
}
render (
return (
<div>
<h2>Parent Component</h2>
<button onClick={this.changeIDs}>Change Data</button>
<ListOfNames objectOfIds={this.props.reduxData.objectOfIds}/>
</div>
)
)
}
function mapStateToProps(state) {
return {
reduxData: state.reduxData
};
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
set: set
}, dispatch)
}
export default connect(mapStateToProps, matchDispatchToProps)(ParentComponent);
Child Compoenent
// React
import React from 'react';
import firebase from 'firebase';
// Redux
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {set} from './../actions/index.js';
// Firebase Database
var databaseRef;
class ListOfNames extends React.Component {
constructor(props) {
super(props);
this.state= {
arrayOfNames: []
}
this.fetchNamesForIds = this.fetchNamesForIds.bind(this);
this.add = this.add.bind(this);
}
componentDidMount() {
console.log("componentDidMount triggering...");
firebase.auth().onAuthStateChanged(function (user) {
if (!user) {
console.log("no user authenticated");
}
databaseRef = firebase.database().ref('/people/' + user.uid);
this.fetchNamesForIds(this.props.reduxData.objectOfIds);
})
}
// I have tried making the fetch in componentWillReceiveProps so that the function would run anytime the IDs were updated in redux, but "this.props.objectOfIds" and "this.props.reduxData.objectOfIds"
componentWillReceiveProps() {
console.log("componentWillReceiveProps triggering...");
console.log("this.props.objectOfIds");
console.log(this.props.objectOfIds);
console.log("this.props.reduxData.objectOfIds");
console.log(this.props.reduxData.objectOfIds);
this.fetchNamesForIds(this.props.reduxData.objectOfIds);
// Note: I have also tried: this.fetchNamesForIds(this.props.objectOfIds); so that the data is passed in from the parent
}
// fetched the names for the associated IDs
fetchNamesForIds(personIds) {
if (personIds === [] || personIds === undefined || personIds === null) {
ALTERNATIVE TO LINE ABOVE
I would prefer to store the data in redux so that it is accessible to other components, but doing this did allow the data to load, but it loads with a lag (i.e. when I change the IDs, it loads the names associated to the previous IDs)
// this.setState({
// arrayOfNames: []
// });
this.props.set("arrayOfNames", []);
return
}
var arrayOfNames = [];
// loop through person and set each value into the arrayOfNames array
Object.keys(IDs).map(function(person, index) {
console.log("person = " + person);
console.log("index = " + index);
// get names associated with the ids obtained
var name = ''
databaseRef.child('people').child(person).limitToFirst(1).on("value", function(snapshot) {
var firstName = snapshot.child('firstName').val()
var lastName = snapshot.child('firstName').val()
name = firstName + " " + lastName
console.log("name = " + name);
arrayOfNames.push(name);
console.log("arrayOfNames = " + arrayOfNames);
this.props.set("arrayOfNames", arrayOfNames);
ALTERNATIVE TO LINE ABOVE
I would prefer to store the data in redux so that it is accessible to other components, but doing this did allow the data to load, but it loads with a lag (i.e. when I change the IDs, it loads the names associated to the previous IDs)
// this.setState({
// arrayOfNames: arrayOfNames
// });
}.bind(this));
}.bind(this));
}
render() {
return(
(this.props.user.arrayOfNames === [] || this.props.user.arrayOfNames === undefined || this.props.user.arrayOfNames === null || this.props.user.arrayOfNames.length < 1)
? <span>no people selected</span>
: <div>
<h5>List of People</h5>
{this.props.user.arrayOfNames.map((name, index) => {
return (
<h5>{name}</h5>
)
})}
</div>
)
}
}
ListOfNames.propsTypes = {
objectOfIds: React.PropTypes.Object
};
function mapStateToProps(state) {
return {
reduxData: state.reduxData
};
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
set: set
}, dispatch)
}
export default connect(mapStateToProps, matchDispatchToProps)(ListOfNames);
Similar Questions:
https://github.com/gaearon/redux-thunk/issues/80
React Native Child Component Not Updated when redux state changes
update child component when parent component changes
Does anyone understand how I can get my component to load the data based on the current IDs in redux?
Probably because the object keys are changed but not the object reference.
A hacky solution would be to call this.forceUpdate() to update the component after the change:)
I had a similar issue where I was loading a child component multiple times on one page and despite passing in what I thought was a unique ID it would only reference the first ID. I know this isn't exactly the situation you have but this will allow you to have a unique object key AND a unique object reference which will hopefully fix your issue.
This is the package I used for this: https://www.npmjs.com/package/react-html-id. When you import the package you need to have curly brackets.
import { enableUniqueIds } from 'react-html-id'
The rest is explained on npm.
Tip: you don't need to bind your functions if you use the new javascript syntax.
this.add = this.add.bind(this); will be solved by writting the add method like:
add = () => {
};
The issue is the Child Component componentWillReceiveProps. You are not using the new props that are propagated to this component. componentWillReceiveProps is called with nextProps, which contains the updated props.
Use this in your child component
componentWillReceiveProps(nextProps) {
console.log("componentWillReceiveProps triggering...");
console.log("nextProps.objectOfIds ", nextProps.objectOfIds);
console.log("nextProps.reduxData.objectOfIds ", nextProps.reduxData.objectOfIds);
this.fetchNamesForIds(nextProps.reduxData.objectOfIds);
}