Correct approach for using flux and component lifecycle - reactjs

I'm migrating the code from what I see on here on CodePen.
Within IssueBox, I am planning to implement a form which an enduser will update setting a state from 'unverified' to 'verified'.
App (ill rename this component) will be my parent and IssueBox would be the child.
So I got through flux => Action -> dispatcher -> udpate db -> update view.
Now that I have the new state and the view should be updated, do I use componentWillRecieveProps() and then setState there, so that in IssueBox I can continue using this.props thus in turn updating it.
import React, { Component } from "react";
import IssueBox from "./issuebox.js";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoaded: false,
email: [],
counter: 0,
title: "Test run"
};
}
componentDidMount() {
fetch(
"https://s3-us-west-2.amazonaws.com/s.cdpn.io/311743/dummy-emails.json"
)
.then(res => res.json())
.then(result => {
const emails = result.data;
console.log("resutl state: ", emails);
let id = 0;
for (const email of emails) {
email.id = id++;
email.verified = 'False'
}
this.setState({
isLoaded: true,
emails: emails
});
});
}
render() {
//console.log(this.state.email);
return (
<div className="App">
<div>
<IssueBox emails={this.state.email} />
</div>
</div>
);
}
}
//issuebox.js
import React, { Component } from "react";
class IssueBox extends Component {
constructor(args) {
super(args);
const emails = this.props.emails;
console.log("inner props: ", emails);
let id = 0;
for (const email of emails) {
email.id = id++;
}
this.state = {
selectedEmailId: 0,
currentSection: "inbox",
emails
};
}
//...copy and pase from codepen
setSidebarSection(section) {
let selectedEmailId = this.state.selectedEmailId;
if (section !== this.state.currentSection) {
selectedEmailId = "";
}
this.setState({
currentSection: section,
selectedEmailId
});
}
componentWillReceiveProps(newProps) {
// Assign unique IDs to the emails
this.setState({ emails: newProps.data });
}
render() {
const currentEmail = this.state.emails.find(
x => x.id === this.state.selectedEmailId
);
return (
<div>
<Sidebar
emails={this.props.emails}
setSidebarSection={section => {
this.setSidebarSection(section);
}}
/>
)}
///.....copy and pase from codepen

The error is being caused by this line in componentWillReceiveProps():
this.setState({ emails: newProps.data });
The emails are coming in on a property called emails so that line should be:
this.setState({ emails: newProps.emails });
That being said, componentWillReceiveProps() gets called more frequently than you might expect. I recommend that you add the id's to the emails within componentDidMount() of App so they come into IssueBox ready to use. This means that App is keeping the emails in its state and simply passing them to IssueBox as props, so you can remove emails from the state in IssueBox and just use the emails that come in through the props everywhere within IssueBox (similar to how the other components use emails coming in on their props and don't keep them in their own local state).

Related

How can I chain asynchronous Firebase updates in my React app?

React & Firebase newbie here. I have a React component that needs to look up some stuff in Firebase before rendering. My database design requires first getting the correct doohick ids and subsequently looking up the doohick details, but I'm not sure how to do that with the asynchronous nature of Firebase database access. This doesn't work:
class Widget extends React.Component {
componentDidMount() {
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
});
this.state.doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
render() {
if (this.state.doohick-ids) {
return null;
} else {
// render the Doohick subcomponents
}
}
}
I can think of a few solutions here, but none that I like. What's the recommended way to chain together Firebase calls, or perhaps redesign this to eliminate the problem?
I think you should split one component Widget to two WidgetList and WidgetItem.
WidgetItem
subscribe and unsubscribe to firebase.database().ref(/doohick-details/${id})
class WidgetItem extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.state = {};
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
const { id } = this.props;
this.dbRef = firebase.database().ref(`/doohick-details/${id}`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
// update state
this.setState(dataSnapshot);
}
render() {
return (
<pre>{JSON.stringify(this.state, null, 2)}</pre>
);
}
}
WidgetList
subscribe and unsubscribe to firebase.database().ref(/users/${username}/doohick-ids)
class WidgetItem extends React.Component {
constructor(props) {
super(props);
this.state = { doohick_ids: [] };
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
// Note: I've just copied your example. `username` is undefined.
this.dbRef = firebase.database().ref(`/users/${username}/doohick-ids`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
this.setState({ doohick_ids: dataSnapshot });
}
render() {
const { doohick_ids } = this.state;
if (doohick_ids.length === 0) {
return 'Loading...';
}
return (
<React.Fragment>
{doohick_ids.map(id => <WidgetItem key={id} id={id} />)}
</React.Fragment>
);
}
}
And code that requires the data from the database needs to be inside the callback that is invoked when that data is available. Code outside of the callback is not going to have the right data.
So:
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
});
There's many optimizations possible here, but they all boil down to the code being inside the callback and updating the state when a value comes from the database.

Update immediately after delete data

class App extends Component{
constructor(props){
super(props);
this.state = {
users:[]
};
}
componentDidMount() {
axios.get(`http://localhost:3000/employees`)
.then(res => {
const users = res.data;
this.setState({ users });
})
}
render(){
return(
<div>
<Main users= {this.state.users}/>
<Form/>
</div>
);
}
}
class Main extends Component{
state = {
id: ''
}
handleChange = event => {
this.setState({ id: event.target.value });
}
handleSubmit = event => {
event.preventDefault();
axios.delete(`http://localhost:3000/employees/${this.state.id}`)
.then(res => {
console.log(res);
console.log("this is" + res.data);
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<label>
Person Name:
<input type="text" name="name" onChange={this.handleChange} />
</label>
<button type="submit">Delete</button>
</form>
</div>
)
}
}
Can someone tell me why after the Axios delete request, how I can render the new state in the users array from App component?
In the App component, I am trying to make this.state.users as a props to send it to the Form component. My guess is put this.setState({users: res.data}). The delete request is fine with 200, but I need to refresh the page to get the new result. How can I update immediatly?
// this is a json object
"employees": [
{
"id": 8,
"first_name": "Lteve",
"last_name": "Palmer",
"email": "steve#codingthesmartway.com"
},
As Dave mentions in a comment you want to have single responsibility for state between components.
This topic is also discussed in the blog post You Probably Don't Need Derived State, where one solution to your problem is to have Main "report back" to App in order to update state. So either App passes down a onDeleteUser function, or a callback for when a user is removed, such as onUserWasDeleted.
The latter can be done with the least amount of changes to your code I suppose.
class App extends Component {
constructor(props) {
super(props);
this.onUserWasDeleted = this.onUserWasDeleted.bind(this);
}
onUserWasDeleted(userId) {
// remove user that was successfully removed
this.setState({ users: this.state.users.filter(user => user.id !== userId) });
}
render() {
return (
<Main
users={this.state.users}
// pass down a callback to Main
onUserDeleted={this.onUserWasDeleted}
/>
);
}
}
class Main extends Component {
handleSubmit = event => {
event.preventDefault();
axios.delete(`http://localhost:3000/employees/${this.state.id}`)
.then(res => {
console.log(res);
console.log("this is" + res.data);
// call our callback function in App
this.props.onUserWasDeleted(this.state.id);
})
}
}

How to fetch data from api and pass it as props

I'm in the learning phase of react and trying to figure out how to
fetch api data and pass it as props, so i created my own api file in
github and tried to fetch the api data from it, here is the link
below:
https://raw.githubusercontent.com/faizalsharn/jokes_api/master/jokesData.js
for some reason the data is not being fetched from the api and not
being passed as props could someone, please explain me where im doing
wrong, forgive me if there is any obvious mistakes here im still in
beginner level
App.js
import React, {Component} from "react"
import Joke from "./joke"
class App extends Component {
constructor() {
super()
this.state = {
loading: false,
jokeComponents: []
}
}
componentDidMount() {
this.setState({loading: true})
fetch("https://raw.githubusercontent.com/faizalsharn/jokes_api/master/jokesData.js")
.then(response => response.json())
.then(data => {
this.setState({
loading: false,
jokeComponents: data.jokesData.map(joke => <Joke key={joke.id} question={joke.question} punchLine={joke.punchLine} />)
})
})
}
render() {
const text = this.state.loading ? "loading..." : this.state.jokeComponents
return (
<div>
{text}
</div>
)
}
}
export default App
joke.js
import React from "react"
function Joke(props) {
return (
<div>
<h3 style={{display: !props.question && "none"}}>Question: {props.question}</h3>
<h3 style={{color: !props.question && "#888888"}}>Answer: {props.punchLine}</h3>
<hr/>
</div>
)
}
export default Joke
I check the API, and found out that it is not working properly when the response.json() is being invoke in the fetch API.
And this is due to the error in the response of the API. You just need to return a bare array, and not return the API with a variable.
For reference, please check the return json of the Jsonplaceholder Fake API. https://jsonplaceholder.typicode.com/posts
Hope this fix your error.
Also, for the state of the jokeComponents, please just have the array passed in the response, and not manipulate the data. Just use the .map for the jokeArray in the render() function if the state is changed. :)
To show content after it is being loaded and hide the loading indicator, use a function that simulates an async action and after that the data will be shown. I've shown this example with another API, as there is a problem with your API. I hope you fix that. Also set headers to allow cross domain data access.
App.js
import React, { Component } from "react";
import ReactDOM from "react-dom";
import Joke from "./Joke";
class App extends Component {
constructor() {
super();
this.state = {
loading: true,
jokeComponents: []
};
}
componentDidMount() {
fetch("https://jsonplaceholder.typicode.com/posts",{
headers: { crossDomain: true, "Content-Type": "application/json" }
}).then(response=>response.json())
.then(data => {
console.log(data);
this.setState({
jokeComponents: data.map(joke => (
<Joke
key={joke.id}
question={joke.title}
punchLine={joke.body}
/>
))
});
});
demoAsyncCall().then(() => this.setState({ loading: false }));
}
render() {
const { loading } = this.state;
if(loading) {
return "loading...";
}
return <div>{this.state.jokeComponents}</div>;
}
}
function demoAsyncCall() {
return new Promise((resolve) => setTimeout(() => resolve(), 2500));
}
ReactDOM.render(<App />, document.getElementById('root'));
The working code of the same is set up in CodeSandbox below:
Gideon Arces correctly explained your first bug, but there's more to do:
You need to format your .json file as json, which is not the same as javascript.
For example, while this is javascript {id: 1, question: "?"} it's not json. Json must be formatted like this: {"id": "1", "question":"?"} with quotes around the property names.
You need to do your data fetching in your componentDidMount and call setState there
You need to pull data from the state and render your components in render(). Typically this is done by creating an array of components and then putting them into the return inside {}. See more on that here: Lists and Keys
It's always a good idea to start with dummy data hardcoded into your component before you try to combine your ui with your api. See below in the componentDidMount() where I hardcoded some jokes. This way you can isolate bugs in your ui code from those in your network/api code.
class App extends React.Component {
constructor() {
super();
this.state = {
loading: false,
jokes: []
};
}
componentDidMount() {
// this.setState({loading: true})
// fetch("https://raw.githubusercontent.com/faizalsharn/jokes_api/master/jokesData.js")
// .then(response => response.json())
// .then(data => {
// console.log(data);
// this.setState({
// loading: false,
// jokes: data
// })
// })
const json = `[
{
"id": "1",
"question": "?",
"punchLine": "It’s hard to explain puns to kleptomaniacs because they always take things literally."
},
{
"id": "2",
"question": "What's the best thing about Switzerland?",
"punchLine": "I don't know, but the flag is a big plus!"
}
]`;
const jokes = JSON.parse(json);
this.setState({ jokes });
}
render() {
const jokeComponents = this.state.jokes.map(joke => (
<Joke key={joke.id} question={joke.question} punchLine={joke.punchLine} />
));
console.log(jokeComponents);
const text = this.state.loading ? "loading..." : jokeComponents;
return <div>Text: {text}</div>;
}
}
function Joke(props) {
console.log("joke props:", props);
return (
<div>
<h3 style={{ display: !props.question && "none" }}>
Question: {props.question}
</h3>
<h3 style={{ color: !props.question && "#888888" }}>
Answer: {props.punchLine}
</h3>
<hr />
</div>
);
}

ReactJS - Pass Updated Value To Sub-Component Method

I'm working on an environment that is basically set up with a Main Component like this:
class MainComponent extends Component {
constructor(props) {
super(props);
this.state = {
selectedValues: []
};
}
render() {
const { selectedValues } = this.state;
return (
// Other components
<SubComponent selectedValues = {selectedValues} />
// Other components
);
}
}
export default MainComponent;
And a Sub Component like this:
class SubComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isExporting: false,
selectedValues: props.selectedValues
};
}
performTask = () => {
this.setState({ isWorking: true });
const { selectedValues } = this.state;
console.log(`Selected Values: ${selectedValues}`);
fetch('/api/work', {
method: 'GET'
})
.then(res => res.json())
.then((result) => {
// Handle the result
this.setState({ isWorking: false });
})
.catch((error) => {
console.log(error);
this.setState({ isWorking: false });
});
};
render() {
const { isWorking } = this.state;
return (
<Button
bsStyle="primary"
disabled={isWorking}
onClick={() => this.performTask()}
>
{isWorking ? 'Working...' : 'Work'}
</Button>
);
}
}
SubComponent.propTypes = {
selectedValues: PropTypes.arrayOf(PropTypes.string)
};
SubComponent.defaultProps = {
selectedValues: []
};
export default SubComponent;
In the Main Component, there are other components at work that can change the selectedValues. The functionality I'd like to see is that when the performTask method fires, it has the most recent and up to date list of selectedValues. With my current setup, selectedValues is always an empty list. No matter how many values actually get selected in the Main Component, the list never seems to change in the Sub Component.
Is there a simple way to do this?
I would suggest you 2 of the following methods to check this problem:
Maybe the state.selectedItems doesn't change at all. You only declare it in the contractor but the value remains, since you didn't setState with other value to it. Maybe it will work if you will refer to this.props.selectedItems instead.
Try to add the function component WillReceiveProps(newProps) to the sub component and check the value there.
If this method doesn't call, it means the selectedItems doesnt change.
Update if some of it works.
Good luck.
selectedValues in SubComponent state has not updated since it was set in SubComponent constructor. You may need to call setState again in componentWillReceivedProps in SubComponent

How to force children rerendering after axios call in React?

I'm working on a form with interactive inputs. They have to actualise themselves with information into parent state.
I use Axios to get the data to show, getting them from an external API. I tried to set default values, but they never actualise with newer values.
class Form extends React.Component {
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState(res.data)
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker />
</form>
</div>
)
}
}
class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: new Date(),
runMin: new Date(),
runMax: new Date()
};
}
getDate() {
console.log('DAD');
try { // if axios didn't finish, to avoid undefined
this.setState({
runMin: super.state.RunMin,
runMax: super.state.RunMax})
} catch (e) {
this.setState({
runMin: new Date(),
runMax: new Date()})
}
}
componentDidMount() {
this.getDate();
this.setState({selected: this.state.runMax});
}
render() {
return (<div></div>);
}
}
Actually after axios call, the children doesn't rerender. I separated the call for axios and the component using it, because the Form component do a single call for multiple children (not displayed here), and they read the parent's state to render.
Firstly, you should not access the parents state using super and instead pass the required value as props
Secondly, componentDidMount lifecycle is executed on initial mount and hence the logic within it won't execute when the parent state updates.
The correct way to handle your case would be
class Form extends React.Component {
state = {
RunMin: new Date(),
RunMax: new Date()
}
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState({RunMin: res.data.RunMin, RunMax: res.data.RunMax})
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker runMin={this.state.RunMin} runMax={this.state.RunMax}/>
</form>
</div>
)
}
}
class DatePicker extends React.Component {
render() {
console.log(this.props.runMin, this.props.runMax);
return (<div></div>);
}
}
The way you are setting the state is incorrect
Change
this.setState(res.data);
To
this.setState({data: res.data});
You need to set the response to a state field you have in component and make sure you pass the data to the child component

Resources