MobX: rerender after assign - reactjs

Good day!
I have an parent component:
#observer
class ToDos extends Component {
componentWillMount() {
this.state = new State();
this.onClearAllCompleted = this.onClearAllCompletedHandler.bind(this);
}
onClearAllCompletedHandler() {
this.state.clearAllCompleted();
}
render() {
const todos = this.state.todos;
return (
<Provider todos={todos}>
{this.props.children}
<ClearAllButton onClick={this.onClearAllCompleted} />
</Provider>
);
}
}
And state class for it:
class TodosState {
#observable todos = [
{ title: 'Sleep', completed: true },
{ title: 'Sleep more', completed: false },
{ title: 'Sleep more than before', completed: false }
];
#action
clearAllCompleted() {
this.todos = this.todos.filter(todo => !todo.completed);
}
}
When i try to clear all completed todos, it clears they with warning in browser console: MobX Provider: Provided store 'todos' has changed. Please avoid replacing stores as the change might not propagate to all children.
After this nothing happens: i have old rendered html ;(
So, i think that childrens has observable object of todos that references to one object and after assign in state i have different ref. Childs dont know about this, and their observable doesn't changed at all. So, what i can do in this case?

The issue is in render method - on each re-render you pass new todos into Provider component. Provider component is tricky component which needs always the same props, but you pass different todos array each time.
Corrected code: pass the whole state object into Provider (this.state object is always the same in your example, just as Provider wants)
render() {
return (
<Provider store={this.state}>
{this.props.children}
<ClearAllButton onClick={this.onClearAllCompleted} />
</Provider>
);
}
By the way I recommend you replace componentWillMount() with constructor(). Constructor is better place for store initialization. Especially in next versions of React (16.0+) componentWillMount() might be called several times for the same instance before actual mount.

Related

In react context need help understanding why in react re-renders all consumers every time the Provider re-renders?

Why is this happening? I am trying to understand why putting the object {something:'something'} directly in value re-renders the consumers.
https://reactjs.org/docs/context.html
Before:
class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
After:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
It's because every time App rerenders, a brand new object is created and passed in as the value. That new object may have the same properties as before, but it's still a new object. The provider uses an object reference comparison to tell whether or not the value has changed (ie, it compares with ===). Since the references are different, it is considered to have changed and the subscribers must rerender with this new value.
const one = { something: 'something' };
const two = { something: 'something' };
console.log(one === two);

Render Props - need to execute function after some component configuration

UPDATE, SOLUTION FOR NOW:
I moved scopes to the state and now the scopes data is up to date.
I am using render prop with the new context API. To make it easier, lets say that API got two methods. Method A is used by ChildComponent via Context API, and methodB is used as render prop.
The problem is that I need on init below order:
ChildComponent runs methodA from context API
Component property: this.scopes is populated
When methodB runs (from render props), it knows about this.scope
For now, methodB runs before the this.scope is populated (this.scope = {}) by methodA
I tried with setTimeout, but I don't think it is the best idea...
class Component extends React.Component{
scopes = {};
render(){
const api = {
methodA: (name, fields) => {
this.scopes[name] = fields;
},
methodB: (name) => {
console.log(this.scopes[name])
}
}
return (
<ComponentContext.Provider value={{ api }}>
{typeof children === 'function' ? children(api) : children}
</ComponentContext.Provider>
);
}
}
/************* CHILD COMPONENT */
class ChildComponent extends React.Component{
static contextType = ComponentContext;
componentWillMount() {
const { api } = this.context;
api.methodA(name, this.fields);
}
render() {
const { children } = this.props;
return children;
}
}
/************* COMPONENTS IMPELENTATION WITH THE PROBLEM */
<Component>
{(api) => (
<ChildComponent name="foo"> // it should at first add foo to the Component this.scope;
<div>Some Child 1</div>
</ChildComponent>
<ChildComponent name="bar"> // it should at first add bar to the Component this.scope;
<div>Some Child 2</div>
</ChildComponent>
{api.methodB(foo)} // not working because for now this.scopes is empty object (should be: {foo: someFields, bar: someFields})
)}
</Component>
I expect to result this.scope = {foo: ...someFields, bar: ...someFields }, for now this.scope= {} after initial run, next invocation of methodB works okey, and (this.scope = {foo: ...someFields, bar: ...someFields}.
Thank you for any tips.
You add and use the scope in the same lifecycle, thus using the old version of passed context. You can move your api.methodB(foo) from Render() method to componentDidUpdate() step, which will ensure you have a new context when it executes.
In case the initialization occurs only once and synchronously, parent Component can be considered initialized when it is mounted.
If the purpose of methodB is to return data that a parent was initialized with, parent's state should be updated on initialization. It's an antipattern to store component's state outside this.state. There may be a flag that definitely indicates that the initialization was completed:
class Component extends React.Component{
state = {
scopes: {},
init: false,
methodA: (name, fields) => {
if (this.state.init) return;
this.state.scopes[name] = fields;
},
methodB: (name) => this.state.init ? this.scopes[name] : null
};
componentDidMount() {
this.setState({ init: true });
}
render(){
return (
<ComponentContext.Provider value={this.state}>
...
</ComponentContext.Provider>
);
}
}
This way api.methodB(foo) will return null, unless the initialization is completed.

React-Redux component state

tl;dr I'm trying to save initial state inside a sub-container component but it gets updated to the new values every time the Redux store gets updated. I probably missed something in configuration and I need help to sort things out.
index.tsx
const store = createStore(reducers, loadedState, enhancer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById(containerId)
)
AppProps.ts
function mapStateToProps(state: StoreState){
return{
... // app props
userDetails: state.userDetails // array of objects fetched by id
}
}
function mapDispatchToProps(dispatch: ReactRedux.Dispatch<actions.AppActions>){
return{
... // app methods
detailsUpdate: (props: UpdateProps) => (dispatch(actions.detailsUpdate(props: UpdateProps)))
}
}
ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App)
Actions.ts
function detailsUpdate(props: UpdateProps): UpdateUserDetails {
return {
type: UPDATE_USER_DETAILS,
props: { ... }
}
}
Reducers.ts
export function reducers(state: StoreState, action: actions.AppActions): StoreState {
let newState: StoreState = {...state};
switch (action.type) {
case actions.UPDATE_USER_DETAILS:
... // reducer code
break;
case actions.UPDATE_PRODUCTS:
... // reducer code
break;
return newState;
}
App.tsx
const App = (allProps: IAppProps, state: StoreState) => {
<UserDetailsContainer
id="generalDetails"
userDetails={allProps.userDetails.byId}
detailsUpdate={allProps.detailsUpdate}
/>
}
UserDetailsContainer.tsx 1
class UserDetailsContainer extends
React.Component<UserDetailsContainerProps, UserDetailsState> {
constructor(props: UserDetailsContainerProps) {
super(props);
this.state = {
userData: props.userDetails[props.id]
}
}
render(){
<input type="text"
value={this.props.userDetails[this.props.id].address}
/>
}
}
detailsUpdate triggers UPDATE_USER_DETAILS action and reducer updates store state with new value. Now, UserDetailsContainer receives updated version of userDetails from the store which is fine for displaying new value in <input type="text"> element.
However, this.state gets updated with new value which I expect shouldn't happen as constructor should be called only once (and is). This prevents me from referencing initial value in case I need it for reset or other.
Please ask for any missing information and/ or clarification and ignore any typos as the app works without errors otherwise.
1 Component usually renders another presentational component for <input type="text">which I omitted here for brevity.
Thanks!
The following only makes a shallow copy of state object.
let newState: StoreState = {...state};
So if you assign
this.state = {
userData: props.userDetails[props.id]
}
And then modify the array inside your reducer, you will also modify the component's state since it references the same object.
This also goes against the concept of redux - reducer should not mutate it's arguments.
Note that this exact mistake is highlighted in the redux docs: https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#common-mistake-2-only-making-a-shallow-copy-of-one-level

componentDidMount() not being called when react component is mounted

I've been attempting to fetch some data from a server and for some odd reason componentDidMount() is not firing as it should be. I added a console.log() statement inside of componentDidMount() to check if it was firing. I know the request to the server works as it should As I used it outside of react and it worked as it should.
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
obj: {}
};
};
getAllStarShips () {
reachGraphQL('http://localhost:4000/', `{
allStarships(first: 7) {
edges {
node {
id
name
model
costInCredits
pilotConnection {
edges {
node {
...pilotFragment
}
}
}
}
}
}
}
fragment pilotFragment on Person {
name
homeworld { name }
}`, {}). then((data) => {
console.log('getALL:', JSON.stringify(data, null, 2))
this.setState({
obj: data
});
});
}
componentDidMount() {
console.log('Check to see if firing')
this.getAllStarShips();
}
render() {
console.log('state:',JSON.stringify(this.state.obj, null, 2));
return (
<div>
<h1>React-Reach!</h1>
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
</div>
);
}
}
render(
<App></App>,
document.getElementById('app')
);
The issue here is that the render method is crashing, because the following line is generating an error
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
Fix this to not use this.state.obj.allStarships.edges[1].node.name directly, unless you can guarantee that each receiver is defined.
Check your component's key
Another thing that will cause this to happen is if your component does not have a key. In React, the key property is used to determine whether a change is just new properties for a component or if the change is a new component.
React will only unmount the old component and mount a new one if the key changed. If you're seeing cases where componentDidMount() is not being called, make sure your component has a unique key.
With the key set, React will interpret them as different components and handle unmounting and mounting.
Example Without a Key:
<SomeComponent prop1={foo} />
Example with a Key
const key = foo.getUniqueId()
<SomeComponent key={key} prop1={foo} />
Also check that you don't have more than one componentDidMount if you have a component with a lot of code. It's a good idea to keep lifecycle methods near the top after the constructor.
I encountered this issue (componentDidMount() not being called) because my component was adding an attribute to the component state in the constructor, but not in the Component declaration. It caused a runtime failure.
Problem:
class Abc extends React.Component<props, {}> {
this.state = { newAttr: false }; ...
Fix:
class Abc extends React.Component<props, {newAttr: boolean}> {
this.state = { newAttr: false }; ...

How to get the state of a React app?

So, I've built up this React app with some state. I want to know what that state is, so I can save it to localStorage and let state carry from one session to another. Basically, the app is pretty complex and I don't want people to lose their "place" just because the closed it and opened it again later.
Reading through the React docs though, I don't see anything that references accessing a component's state from outside of React.
Is this possible?
You should never ever try to get a state from a component as a component should always be representing and not generate state on its own. Instead of asking for the component's state, ask for the state itself.
That being said, I definitely see where you're coming from. When talking about React, the term "state" seems to be pretty ambiguous indeed.
I don't see anything that references accessing a component's state
from outside of React.
Once and for all, here's the difference:
Local state, shouldn't persist: this.state, this.setState et al. Local state lives only within the component and will die once the component dies.
Global state, can be persisted: this.props.passedState. Global state is only passed to the component, it can not directly modify it. The view layer will adjust to whatever global state it got passed.
Or in simple:
this.state means local state, won't get persisted.
this.props.state means passed state, could be persisted, we just don't know and we don't care.
Example
The following example uses stuff of Babel's stage-1 Preset
React is all about giving a display representation of a data structure. Let's assume we have the following object to visibly represent:
let fixtures = {
people: [
{
name: "Lukas"
},
{
name: "fnsjdnfksjdb"
}
]
}
We have an App component in place, that renders Person components for every entry in the people array.
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
class Person extends Component {
static propTypes = {
name: PropTypes.string.isRequired
}
render() {
return (
<li>
<input type="text" value={this.props.name} />
</li>
);
}
}
class App extends Component {
static propTypes = {
people: PropTypes.array.isRequired
}
render() {
let people = this.people.map(person => {
<Person name={person.name} />
});
return (
<ul>
{people}
</ul>
);
}
}
ReactDOM.render(
<App people={fixtures} />,
document.getElementById('yourid')
);
Now, we will implement focus functionality. There are 2 options:
We don't care about which person was focused the last time the user used the app, so we use local state.
We do care about which person was focused the last time the user used the app, so we use global state.
Option 1
The task is simple. Just adjust the People component so that it knows (and rerenders) once the focus changed. The original data structure won't be changed and the information whether a component is focused or not is
merely client side information that will be lost once the client is closed/reset/whatever.
State is local: We use component.setState to dispatch changes to local state
class Person extends Component {
static propTypes = {
name: PropTypes.string.isRequired
}
constructor(...args) {
super(...args);
this.state = {
isFocused: false
}
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
}
static propTypes = {
name: PropTypes.string.isRequired
}
onFocus() {
this.setState({
isFocused: true;
});
}
onBlur() {
this.setState({
isFocused: false;
});
}
render() {
let borderColor = this.state.isFocused ? '#ff0' : '#000';
style = {
border: `1px solid ${borderColor}`
}
return (
<li>
<input
style={style}
type="text"
value={this.props.name}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</li>
);
}
}
Option 2
We actually want to persist the focused element to whatever store (eg. backend) we have, because we care about the last state. State is global: React components receive only props as "state", even granular information as whether an element is focused. Persist and feed global state to the app and it will behave accordingly.
function setFocus(index) {
fixtures.people[index].isFocused = true;
render();
}
function clearFocus(index) {
fixtures.people[index].isFocused = false;
render();
}
function render() {
ReactDOM.render(
<App people={fixtures} />,
document.getElementById('yourid')
);
}
class Person extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
isFocused: PropTypes.bool,
index: PropTypes.number.isRequired
}
static defaultProps = {
isFocused: false
}
constructor(...args) {
super(...args);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
}
static propTypes = {
name: PropTypes.string.isRequired
}
onFocus() {
setFocus(this.props.index);
}
onBlur() {
clearFocus(this.props.index);
}
render() {
let borderColor = this.props.isFocused ? '#ff0' : '#000';
style = {
border: `1px solid ${borderColor}`
}
return (
<li>
<input
style={style}
type="text"
value={this.props.name}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</li>
);
}
}
class App extends Component {
static propTypes = {
people: PropTypes.array.isRequired
}
render() {
let people = this.people.map((person, index) => {
<Person name={person.name} index={index} isFocused={person.isFocused} />
});
return (
<ul>
{people}
</ul>
);
}
}
render();
I think the solution to your problem really lies in how your application is modelled.
Ideally what you would need (depending on complexity) would be a single (flux/redux) store upon which you could subscribe to changes, if it diffs then save it to localStorage.
You would then need to determine a way to bootstrap this data into your single store.
Their is no API per se (that I know of) to do specifically what you want.
Don't try to get the state from outside of React -- pass the state from React to wherever it needs to be.
In your case, I would do this componentDidUpdate.
var SampleRootComponent = React.createClass({
componentDidUpdate: function() {
localStorage.setItem(JSON.stringify(this.state))
}
})
You're right in that there is no way to get the state from outside; you have to set it from React. But there is no reason to view this as a bad thing -- just call an external function from inside of React if you need to pass the info to something else, as shown. No functionality is lost.

Resources