Is there any way to do something like this with mobx?
componentWillUpdate(nextProps) {
if (this.props.prop !== nextProps.prop) {
/* side effect, ex. router transition */
}
}
From my experience, this.props.prop is always equal to nextProps.prop even in componentWill- hooks...
UPD Here is more specific use case — simple login scenario:
Store
class AppStore {
#observable viewer = new ViewerStore();
}
class ViewerStore {
#observable id;
#observable name;
#observable error;
fromJSON(json = {}) {
this.id = json.id;
this.name = json.name;
}
clearError() {
this.error = null;
}
login({ email, password }) {
fetch('/login', {
method: 'POST',
body: JSON.stringify({
email,
password
}),
credentials: 'same-origin'
})
.then(json => {
this.fromJSON(json);
})
.catch(({ error }) => {
this.error = error;
})
}
}
React part
class LogInPage extends Component {
componentWillUpdate(nextProps) {
if (nextProps.viewer.id && !this.props.viewer.id) {
this.props.viewer.clearError();
this.props.onSuccess();
}
}
login = e => {
e.preventDefault();
this.props.viewer.login({
email: e.target.elements['email'].value,
password: e.target.elements['password'].value,
});
}
render() {
return (
<div>
<h1>Log In</h1>
<form onSuccess={this.login}>
<input type="text" name="email" />
<input type="password" name="password" />
<input type="submit" />
</form>
{ this.props.viewer.error && (
<div>
<b>Error</b>: {this.props.viewer.error}
</div>
)}
</div>
);
}
}
const store = new AppStore();
ReactDOM.render(<LogInPage
viewer={store.viewer}
onSuccess={() => alert("Hello!")}
/>
, container);
So basically, I just want to be able to do something when viewer id switches from undefined to something
Thanks for the update, that clarifies the question a lot. It depends a bit on what you want to do, but if you want to change the rendering of your component, it is enough to decorate your component with #observer from the mobx-react package. It will then automatically re-render when the store changes.
If you want do any additional actions, you can setup an autorun in your componentWillMount. Like
componentWillMount() {
this.autorunDisposer = mobx.autorun(() => {
if (nextProps.viewer.id && !this.props.viewer.id) {
this.props.viewer.clearError();
this.props.onSuccess();
}
})
}
But I don't see this pattern very often, as often it is cleaner to have this kind of logic just in your store. Personally I would expect a login form component to rougly look like this:
#observer class RequiresLogin extends Component {
render() {
if (this.props.viewerStore.id !== undefined) {
return this.props.children
} else {
return <LoginPage viewer={this.props.viewerStore}/>
}
}
}
#observer class MyTestPage extends Component {
render() {
return <RequiresLogin viewer={this.props.viewerStore}>
Secret Area!
</RequiresLogin>
}
}
#observer class LogInPage extends Component {
login = e => {
e.preventDefault();
this.props.viewer.login({
// tip: use React refs or the onChange handler to update local state:
email: e.target.elements['email'].value,
password: e.target.elements['password'].value,
});
}
render() {
if (this.props.viewer.id)
return null; // no need to show login form
return (
<div>
<h1>Log In</h1>
<form onSuccess={this.login}>
<input type="text" name="email" />
<input type="password" name="password" />
<input type="submit" />
</form>
{ this.props.viewer.error && (
<div>
<b>Error</b>: {this.props.viewer.error}
</div>
)}
</div>
);
}
}
In 99,9% of cases, this happens because somewhere else in your code props are mutated directly. And this is not allowed in react.
Typical scenario's include:
var myObject = this.props.someObject; // myObject is a POINTER to props, not copy
myObject.attribute = newAttribute; // Now props are directly mutated
var myArray = this.props.someArray;
myArray.splice(a,b); // splice directly mutates array
To solve, make a proper copy of the props object before you update the prop.
Well, actually the problem happened because I passed a mobx store as the single prop (in order to call it's methods like this.props.store.update()), so even though when something changes in store, component is updated by mobx, nextProps still holds the same reference to that store.
I finally ended up with destructuring store into component's props, when I need this kind of check.
Related
I have an app with one child component that I would like to re-render when setState updates the bookInput in the parent's state. I am using axios to request info from google's book api. For some reason, even though the state is updating, the child is not re-rendering. Please help if you can! Thank you!
import React, { Component } from 'react';
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = {
bookInput: 'ender',
bookSubmitted: 'initial'
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSubmitEmpty = this.handleSubmitEmpty.bind(this);
}
handleChange(e) {
this.setState({bookInput: e.target.value});
console.log(this.state.bookInput);
//this.setState({bookSubmitted: false});
}
handleSubmit(e) {
e.preventDefault();
//this.setState({bookSubmitted: true})
const name = this.state.bookInput;
this.setState({bookInput: name});
console.log(this.state);
this.setState({bookSubmitted: 'userSub'});
}
handleSubmitEmpty(e) {
alert('please enter an item to search for');
e.preventDefault();
}
render() {
return (
<div className="App">
<header className = "App-header">
<h1>Book Search App</h1>
</header>
<form className = "form-style" onSubmit = {this.state.bookInput ? this.handleSubmit: this.handleSubmitEmpty}>
<label>
<input type="text" className = "input-style"
value = {this.state.bookInput} onChange = {this.handleChange}>
</input>
</label>
<button type="submit">search books</button>
</form>
{/* <Book bookInput = {this.state.bookInput}/> */}
{/*this.state.bookSubmitted && <Book bookInput = {this.state.bookInput}/>*/}
{
(this.state.bookSubmitted === 'initial' || this.state.bookSubmitted === 'userSub') &&
<Book bookInput = {this.state.bookInput}/>
}
</div>
);
}
}
export default App;
class Book extends Component {
constructor(props) {
super(props);
this.state = {
//bookInput2: "ender",
bookTitles: [],
bookExample: '',
isLoading: false
}
this.bookClick = this.bookClick.bind(this);
}
bookClick(book) {
console.log(book);
console.log(book.volumeInfo.infoLink);
const bookURL = book.volumeInfo.infoLink;
window.open(bookURL);
}
componentDidMount() {
//this.setState({ isLoading: true });
this.setState({isLoading: true});
axios.get(`https://www.googleapis.com/books/v1/volumes?q=${this.props.bookInput}`)
.then((response) => {
const bookExample1 = response.data.items;
console.log(bookExample1);
this.setState({bookTitles: bookExample1, isLoading: false});
})
.catch((error) => {
console.error('ERROR!', error);
this.setState({isLoading: false});
});
}
render() {
return (
<div>
{ this.state.bookTitles ? (
<div>
<h2>book list</h2>
{<ul className = 'list-style'>
{this.state.isLoading &&
(<div>
loading book list
</div>)
}
{this.state.bookTitles.map(book => (
<li key={book.id}>
<span className = 'book-details book-title' onClick = {() => this.bookClick(book)}> {book.volumeInfo.title}</span>
<br/>
{book.volumeInfo.imageLinks &&
<img src = {book.volumeInfo.imageLinks.thumbnail}/>
}
{ book.volumeInfo.description &&
<span className = 'book-details'>{book.volumeInfo.description}</span>
}
<br/>
<span className = 'book-details'>Categories {book.volumeInfo.categories}</span>
</li>
))}
</ul>}
</div>) :
(<p>sorry, that search did not return anything</p>)}
</div>
);
}
}
May be you are looking for something similar to this?
https://stackblitz.com/edit/react-snoqkt?file=index.js
The above code can be simplified more and organized but it gives you some idea.
Main changes in the code.
Changed Api call from componentDidMount lifecycle event to a new method named getInitialdata which is called in handleSubmit.
getInitialdata(name){
axios.get(`https://www.googleapis.com/books/v1/volumes?q=${name}`)
.then((response) => {
const bookExample1 = response.data.items;
console.log(bookExample1);
this.setState({bookTitles: bookExample1, isLoading: false, bookSubmitted: 'userSub'});
})
.catch((error) => {
console.error('ERROR!', error);
this.setState({isLoading: false, bookSubmitted: 'userSub'});
});
}
Changed the way how Child component is used.
<Book bookTitles={this.state.bookTitles} isLoading={this.state.isLoading}/>
Issue with your code is you are making an API call in your component's didMount method. This lifecycle event will be invoked only when the component is mounted. Not when it is updated.
When you enter some input in your textbox and click on "Search books", componentDidMount event doesnt fire. And this is the reason why API calls are not happening from the second time.
More on the lifecycle events at https://reactjs.org/docs/react-component.html#componentdidmount
I've taken your code and extrapolated it into this sandbox. Just as you said, your parent component state is updating as it should, but the problem is that the child component doesn't change its state.
A state change will always trigger a re-render in React. The only problem is, your child component is managing it's own state, which isn't directly changing. Instead, it's just receiving new props again and again, but not doing anything with them.
If you look at your code for the <Book /> component, you only modify its state on componentDidMount, which only happens once. If you'd like to programmatically make it update, you can do one of two things.
Remove state from the child component, and make it rely entirely on props, so that it stays in sync with the parent
Use the componentDidUpdate lifecycle method (docs) to choose when to change the state of the child (which will trigger the re-render)
after getting data from API I want to show them into inputs,edit and update it in DB. I thought that beside the redux state, I should use also local state , but some people here say that is not good practise .So how I can handle my onChange methods and how pass updated data into axios.put method???
class ArticleEdit extends Component {
articleID = this.props.match.params.articleID;
state={
title:'',
text:'',
imgs:[]
}
onChange =(e)=>{}
componentDidMount(){
this.props.getArticleDetails(this.articleID);//get data from API
}
render() {
return (
<Fragment>
{this.props.article===undefined?(<Spin/>):
(
<div >
<div >
<Form onSubmit={this.handleSubmit}>
<Input name='title'
value='this.props.article.title'
onChange={this.onChange}/>
<Textarea
name='text'
value={this.props.article.title}
onChange={this.onChange}/>
<Button htmlType='submit'>Update</Button>
</Form>
</div>
</div>
)}
</Fragment>
)
}
}
const mapStateToProps = state =>({
article: state.articleReducer.articles[0],
})
export default connect(mapStateToProps,{getArticleDetails})
(ArticleEdit);
So how I can handle my onChange methods and how pass updated data into
axios.put method???
Well if that's literally what you want to do, then you can do it like this:
onChange = e => {
try {
const results = await axios.put(someurl, e.target.value)
console.log('results', results)
} catch(e) {
console.log('err', e)
}
}
This will call axios.put after every keystroke - however, I doubt that is what you want.
I've found the solution. I used the static method getDerivedStateFromProps,which calls everytime when props of your component has been changed.
static getDerivedStateFromProps(nextProps, prevState){
if(prevState.title===null && nextProps.article!==undefined){
return{
title:nextProps.article.title,
text:nextProps.article.text
}
}
return null;
}
after that is easy to work with onChange method ,which just call this.setState().
function NewItem(props) {
return (
<div>
<input
id = "content"
placeholder = "add a new item..."
/>
<input
id = "score"
placeholder = "score(points per hour)"
/>
<button
onClick = {
(e) => props.onAddItem(e)
}
>
add
</button>
</div>
);
}
The button click handler is implemented in father class, what I'm trying to do is when I click "add", the father class could be able to get the values of these two inputs, so that it could add an item in its "itemList" state. Is there a good way for me to get the values? I know I can manipulate DOM to do so, but I guess it's not a good way.
Below is the click handler and the render method of father class:
handleAddItem(e) {
const newList = this.state.itemList;
const itemCount = this.state.itemCount;
newList.unshift({
itemInfo: {
content: ,
score: ,
time: ,
}
key: itemCount,
index: itemCount,
});
this.setState({
itemList: newList,
itemCount: itemCount + 1,
});
}
render() {
return (
<div id = "todo">
<NewItem
onAddItem = {
(e) => this.handleAddItem(e)
}
/>
<ItemList
itemList = { this.state.itemList }
onClick = {
(e) => this.handleDeleteItem(e)
}
/>
</div>
)
}
what I'm trying to do is when I click "add", the father class could be able to get the values of these two inputs
One solution is to wrap your inputs in a <form> and send it all together.
function NewItem(props) {
return (
<form onSubmit={props.onAddItem}>
<input name="content"/>
<input name="score"/>
<button type="submit">add</button>
</form>
);
}
class Parent extends Component {
handleAddItem(e) {
e.preventDefault();
const content = e.target.content.value;
const score = e.target.score.value;
// ...
}
render() {
return (
<NewItem onAddItem={this.handleAddItem}/>
);
}
}
You might want to check out references or "refs". I generally avoid trying to use them, but sometimes it is just a cleaner way to deal with a problem.
Here's a snippet that might help you out.
import React from 'react'
class TestComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
controlledValue: 'controlled'
}
this._handleControlledChange = this._handleControlledChange.bind(this)
}
_handleControlledChange(e) {
this.setState({
controlledValue: e.target.value
})
}
render(){
return (
<div>
{/* Bind this to 'this.field1' */}
<h3>Function Binding</h3>
<input ref={f => this.field1 = f} />
{/* bind this to this.refs.field2 */}
<h3>Bind to Refs</h3>
<input ref='field2' />
<h3>Controlled Component</h3>
<input
value={this.state.controlledValue}
onChange={this._handleControlledChange}
/>
<button
onClick = {
e => {
let field1Value = this.field1.value
let field2Value = this.refs.field2.value
alert('Field 1 ' + field1Value + '\n Field 2 ' + field2Value + '\nConrolled: ' + this.state.controlledValue )
}
}
>
Ref test
</button>
</div>
)
}
}
Basically what's going on is I'm binding the component to the class so I can reference it later. Typically React code is going to depend on the state and not allowing components to manage themselves, but sometimes this is the behavior you want (a one off form or something you don't want to manage the state for).
Hopefully this helps. I demonstrated the three main ways you will likely want to control your components. Check out projects like https://material-ui.com/ and the tutorials for some more examples.
#wdm's form is a clever solution, I have not used that but I like it.
I am building a component that will be used for step-through processes such as :
This Workflow component takes an array of 'steps' as a prop and then it does the rest. Here is how it is being called in the image above :
let steps = [
{
display: "Sign Up Form",
component: SignupForm
},
{
display: "Verify Phone",
component: VerifyPhone
},
{
display: "Use Case Survey",
component: UseCase
},
{
display: "User Profile",
component: UserProfile
},
];
return (
<Workflow
steps={steps}
/>
);
The component field points to the component to be rendered in that step. For example the SignupForm component looks like this :
export default class SignupForm extends React.Component {
...
render() {
return (
<div>
<div className="page-header">
<h1>New User Sign Up Form</h1>
<p>Something here...</p>
</div>
<div className="form-group">
<input type="email" className="form-control" placeholder="Email address..." />
<small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
</div>
);
}
}
The issue I'm facing is that in each step there needs to be a Next button to validate the information in that step and move to the next. I was going to just put that button inside the component of each step, but that makes it less user-friendly. When a user clicks 'Next', and everything is valid, that step should be collapsed and the next step should open up. However this means that my Workflow component needs to render this button.
So, I need my Workflow component to call the method of each step component to validate the information in the step and return a promise letting it know if it passed or failed (with any error message). How do I need to call this method? Here is where the Workflow component renders all the steps
as <step.component {...this.props} />:
{
this.state.steps.map((step, key) => {
return (
...
<Collapse isOpen={!step.collapsed}>
<step.component {...this.props} />
<Button color="primary"
onClick={() => this.validate(key)}>Next</Button>
<div className="invalid-value">
{step.error}
</div>
</Collapse>
...
);
})
}
That renders the next button, as well as the onClick handler validate():
validate(i) {
let steps = _.cloneDeep(this.state.steps);
let step = steps[i];
step.component.handleNext().then(function () {
...
}).catch((err) => {
...
});
}
Ideally, step.component.validate() would call the validate method inside that component that has already been rendered:
export default class SignupForm extends React.Component {
....
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
.. which would have access to the state of that component. But, thats not how it works. How can I get this to work? I read a little about forwarding refs, but not exactly sure how that works. Any help is greatly appreciated!
One approach is to apply the Observer pattern by making your form a Context Provider and making it provide a "register" function for registering Consumers. Your consumers would be each of the XXXForm components. They would all implement the same validate API, so the wrapping form could assume it could call validate on any of its registered components.
It could look something like the following:
const WorkflowContext = React.createContext({
deregisterForm() {},
registerForm() {},
});
export default class Workflow extends React.Component {
constructor(props) {
super(props);
this.state = {
forms: [],
};
}
deregisterForm = (form) => {
this.setState({
forms: this.state.forms.slice().splice(
this.state.forms.indexOf(forms), 1)
});
};
registerForm = (form) => {
this.setState({ forms: [ ...this.state.forms, form ] })
};
validate = () => {
const validationPromises = this.state.forms.reduce(
(promises, formComponent) => [...promises, formComponent.validate()]);
Promise.all(validationPromises)
.then(() => {
// All validation Promises resolved, now do some work.
})
.catch(() => {
// Some validation Promises rejected. Handle error.
});
};
render() {
return (
<WorkflowContext.Provider
value={{
deregisterForm: this.deregisterForm,
registerForm: this.registerForm,
}}>
{/* Render all of the steps like in your pasted code */}
<button onClick={this.validate}>Next</button
</WorkflowContext.Provider>
);
}
}
// Higher-order component for giving access to the Workflow's context
export function withWorkflow(Component) {
return function ManagedForm(props) {
return (
<WorkflowContext.Consumer>
{options =>
<Component
{...props}
deregisterForm={options.deregisterForm}
registerForm={options.registerForm}
/>
}
</WorkflowContext.Consumer>
);
}
}
SignupForm and any other form that needs to implement validation:
import { withWorkflow } from './Workflow';
class SignupForm extends React.Component {
componentDidMount() {
this.props.registerForm(this);
}
componentWillUnmount() {
this.props.deregisterForm(this);
}
validate() {
return new Promise((resolve, reject) => {
resolve();
})
}
render() {
...
}
}
// Register each of your forms with the Workflow by using the
// higher-order component created above.
export default withWorkflow(SignupForm);
This pattern I originally found applied to React when reading react-form's source, and it works nicely.
I cannot for the life of me figure out what is wrong with the following code, when a user adds a bug via the BugAdd form, the values are passed to the handleSubmit function which in turn should pass its props to addBug.
However, when I submit my form I see the 'console.log("Adding bug:", bug);'
But then after this I receive "react.min.js:14 Uncaught TypeError: Cannot read property 'bugs' of undefined", my initial thought was that perhaps I have missed a .bind somewhere.
Can anyone spot an issue with my code, it was working fine before refactoring to ES6
class BugAdd extends React.Component {
render() {
console.log("Rendering BugAdd");
return (
<div>
<form name="bugAdd">
<input type="text" name="owner" placeholder="Owner" />
<input type="text" name="title" placeholder="Title" />
<button onClick={this.handleSubmit.bind(this)}>Add Bug</button>
</form>
</div>
)
}
handleSubmit(e) {
e.preventDefault();
var form = document.forms.bugAdd;
this.props.addBug({owner: form.owner.value, title: form.title.value, status: 'New', priority: 'P1'});
// clear the form for the next input
form.owner.value = ""; form.title.value = "";
}
}
class BugList extends React.Component {
constructor() {
super();
this.state = {
bugs: bugData
}
}
render() {
console.log("Rendering bug list, num items:", this.state.bugs.length);
return (
<div>
<h1>Bug Tracker</h1>
<BugFilter />
<hr />
<BugTable bugs={this.state.bugs} />
<BugAdd addBug={this.addBug} />
</div>
)
}
addBug(bug) {
console.log("Adding bug:", bug);
// We're advised not to modify the state, it's immutable. So, make a copy.
var bugsModified = this.state.bugs.slice();
bug.id = this.state.bugs.length + 1;
bugsModified.push(bug);
this.setState({bugs: bugsModified});
}
}
When you extend React.Component with ES6 class, the component methods are not autobinded to this like when you use React.createClass. You can read more about this in the official documentation.
In your case, the cleanest solution is to bind the addBug method in the constructor to the component's this, like this:
class BugList extends React.Component {
constructor() {
super();
this.state = {
bugs: bugData
}
this.addBug = this.addBug.bind(this);
}
render() {
console.log("Rendering bug list, num items:", this.state.bugs.length);
return (
<div>
<h1>Bug Tracker</h1>
<BugFilter />
<hr />
<BugTable bugs={this.state.bugs} />
<BugAdd addBug={this.addBug} />
</div>
)
}
addBug(bug) {
console.log("Adding bug:", bug);
// We're advised not to modify the state, it's immutable. So, make a copy.
var bugsModified = this.state.bugs.slice();
bug.id = this.state.bugs.length + 1;
bugsModified.push(bug);
this.setState({bugs: bugsModified});
}
}
Now you will be able to access this.state.
try to define your addBug method like this with => which will auto bind to the class instance:
addBug = (bug) => {
console.log("Adding bug:", bug);
// We're advised not to modify the state, it's immutable. So, make a copy.
var bugsModified = this.state.bugs.slice();
bug.id = this.state.bugs.length + 1;
bugsModified.push(bug);
this.setState({bugs: bugsModified});
}
don't forget to add the Class properties transform to your babel
http://babeljs.io/docs/plugins/transform-class-properties/