Problem
public componentDidMount() {
// after component mounts I want to get updates from RX source
const that = this;
SomeExternalObservable.subscribe((value) => {
that.setState((prev, props) => ({
currentValue: value
});
});
}
In some cases, I get warning Cannot update during an existing state transition” React with Observable.
I checked SO for answers to that problem, but most of them just suggested to move my code to componentDidMount.
What happens here
it's not obvious, but subscription on observable can be executed (it's very likely to) synchronously (see here https://github.com/tc39/proposal-observable/issues/38)
it's even more likely if an observable has already set value (like BehaviourSubject - see tip here BehaviorSubject vs Observable?)
The problem occurred as well when using libraries like https://github.com/jayphelps/react-observable-subscribe.
An alternative would be to use props instead of setState.
For managing React state with Rx I prefer to use the mapPropsStream recompose helper. It can be a bit confusing if you don't tend to use HOCs, but it is actually pretty simple.
const withSomeExternalValue = mapPropsStream(props$ =>
Observable.combineLatest(
props$,
someExternal$.startWith(null),
(props, someExternalValue) => ({...props, someExternalValue})
)
);
const MyEnhancedComponent = withSomeExternalValue(MyComponent);
MyEnhancedComponent is a new Component type. When it will mount, it will also subscribe to the external Observable. Any emitted value will render the component with the new value as prop.
Additional explanation and notes:
The props$ is an Observable of props. Any change of a prop will result in a new item emitted for prop$
The returned Observable is a new props Observable that its emitted items will be used to render the enhanced component.
The .startWith(null), is to make sure the enhanced component will be rendered before the first value of someExternal$ is emitted. In this case, the someExternalValue prop will start with null
withSomeExternalValue HOC can be reused if these external values are needed by another component. withSomeExternalValue(MyAnotherComponent)
The mapPropsStream can be used with other Observable implementation - so it should be configured. Here is how to use it with Rx -
Can be configured globally:
import rxjsconfig from 'recompose/rxjsObservableConfig'
setObservableConfig(rxjsconfig)
Or by using mapPropsStreamWithConfig:
import rxjsConfig from 'recompose/rxjsObservableConfig'
const mapPropsStream = mapPropsStreamWithConfig(rxjsConfig)
I post the question with the answer as it seems allowed, supported
here
https://meta.stackexchange.com/questions/17463/can-i-answer-my-own-questions-even-if-i-knew-the-answer-before-asking;
spent some time to find the issue and couldn't find similar SO post,
so I hope it will help someone.
Solution
Because we can't expect the subscription to be triggered asynchronously working solution is to force it, i.e. by using Scheduler:
public componentDidMount() {
// after component mounts I want to get updates from RX source
const that = this;
SomeExternalObservable
.subscribeOn(Scheduler.asap) // here
.subscribe((value) => {
that.setState((prev, props) => ({
currentValue: value
});
});
}
Why
componentDidMount happens directly after rendering and change to state will cause additional render - which can happen in the same tick (https://reactjs.org/docs/react-component.html#componentdidmount)
setState should work asynchronously, but doesn't have to; React will decide about it at runtime (https://reactjs.org/docs/react-component.html#setstate)
Subscription on Observable by default is synchronous.
Wrap up question
Is there React point of view right way to fix that issue, different than solution presented?
Related
I have a large and deep object which I render using a React functional component (composed of child components). I also use Redux in the project which interacts with an API. To keep the app fast, I keep a local copy of the object in the component state and dispatch changes when occurred. The objects can also change in different parts of the app.
Each time the object changes in Redux its lastUpdated field is updated with the current time (epoch) by Redux. So instead of doing a deep (and expensive) diff on the whole object it is sufficient to compare the object id (in case the component has to display a different object) and the lastUpdated value (in case the object itself got changed).
This is what my functional component looks like:
interface Item {
id: string
lastUpdated: number
// more fields here
}
interface Props {
item : Item
}
export default function ItemPage(props: Props){
const [displayItem, setDisplayItem] = useState<Item>(props.item)
useEffect(() => {
if (props.item.id !== display.item.id && props.item.lastUpdated !== displayItem.lastUpdated){
setDisplayItem(props.item)
// ... some logic comes here
}
}, [props.item.id, props.item.lastUpdated])
return (
// ... render code here
)
}
This code cause the following error:
React Hook useEffect has missing dependencies: 'item.id' and
'item.lastUpdated'. Either include them or remove the dependency array
react-hooks/exhaustive-deps
I have disable the error with:
// eslint-disable-next-line react-hooks/exhaustive-deps
Questions:
Is it safe to to disable the error as I know the logic of my Redux (and use the more efficient diff)? Is there a better solution?
Is it safe to completely remove the useEffect array as I make the id/lastUpdated diff myself?
Note: I am not looking for the general "do not disable as it will come back to bite you ..." answer.
Looks like you need React.memo with second argument isEqual. Is equal works like shouldComponentUpdate. Read reactjs docs more about React.memo
UPDATE:
I've added codesandbox as example
Can someone explain to me what is the real difference and why both of the example here are working the same:
1) Change a state of observable via action/runInAction inside the store file:
colorStore file:
#observable
color='red'
#action
setColor(){
this.color='blue'
}
2)Change the state via the component itself (which assumed to be bad practice):
React Component file:
onClick = () => this.props.colorStore.color='blue' //still working...
Mobx action is doing the batching, similarly to how ReactJS batches multiple changes.
When you use reactjs click handler react is automatically batching changes that happen inside it, so you will not see component rendering multiple times, however, if you call setColor from some other event, let's say after loading some data, and have multiple calls to change the observable inside the setColor that will trigger the observer three times, and the component will be rendered three times.
When you wrap your function with #action decorator or you use runInAction function, only the last value will be used (green in the code below) and the component will be rendered only once.
setColor(){
// this will render three times
this.color='blue'
this.color='red'
this.color='green'
}
vanilla mobx example that reacts only once:
import { runInAction, observable, reaction, toJS } from "mobx";
const test = observable({
name: "sonic",
nick: "speed demon"
});
// console.log will be called only once with the value "name: 3"
reaction(
() => toJS(test),
data => console.log(`name: ${data.name}`)
);
runInAction(() => {
test.name = "1";
test.name = "2";
test.name = "3";
});
view on codesandbox
Also check out the discussion on the github repo: is #action really necessary?
The difference is more related to conventions and writing clean maintainable code than in the behavior of the program. So:
You are mutating store data in your UI component. This is not correct, because the UI should only visualize data and handle for example user actions which are then translated to certain action in the data store(in your case updating a color). It's store responsibility to manage the state of this data
All observable data is considered a good practice to be mutated only in actions. Why? Because the single source of mutating data is clear - only in actions and only in a data layer(the store). The code is less prone to errors and unclear state management and the great benefit is that you can enforce your application not to build if you turn on use strict mode
I got two screens and want to pass the state from one to the other.
Simplified Screen 1:
import React, { Component } from 'react';
import { View, TextInput} from 'react-native';
import { buttons } from '../../components/buttons/Buttons';
export default class LoginScreen extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
}
}
render() {
return(
<View>
...
<TextInput onChange={(input) => this.setState({username: input})}></TextInput>
...
<Button.main onPress={() => this.props.navigation.navigate('Register', {username: this.state.username})} title='Create Account'/>
</View>
)
}
}
Now, if I press the button, I want to pass this.state.username to the new screen. If I don't use onChange={(input) => this.setState({username: input})} and set a value for the username manually like this:
this.state = {
username: 'Test',
}
The value gets passed without problem and I can access it from the new screen. But when I try to change this.state.username while the TextInput changes and then pass this.state.username I get the following warning:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property isTrusted on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
I don't know how to use event.persist() as proposed in the warning and the username doesn't get passed.
Sorry if this is a stupid question, but I couldn't find a solution and I'm a beginner. I would be glad if someone could help me with this :)
Try using onChangeText instead of onChange.
Adding on to accepted answer to provide a more elaborate explanation.
The behaviour observed is a combination React's intended design for events and how Javascript uses the pass-by-reference concept for passing objects into function:
React's design for events
The context here is that the function is passed to the onChange handler rather than onChangeText. onChange will pass an event object to the function while onChangeText passes a String. There is a catch to how event objects works in React.
React uses SyntheticEvent for events passed into event handlers and SyntheticEvents are pooled. So rather than creating new SyntheticEvent objects, these pooled objects are reused and all properties will be nullified after a callback is invoked (see React docs).
Pass-by-reference for objects in Javascript
In Javascript, when an object gets passed into a function as argument, the function receives the reference to that object as parameter rather than creating a copy of the object. Thus, modifying the object in the function will lead to the original object being modified.
Putting them together
Setting the passed event object parameter to state is basically storing the reference to one of the event objects in the pool into state. Since this object is reused (modified) and its content does not persist for long, the content retrieved from the reference stored in the state will quickly become irrelevant. This will cause the app to behave in unexpected ways.
The development environment detects this and thus give a warning.
The use of onChangeText - like the accepted answer recommended - will not only solve the problem but also seemingly be closer the original intention of the code.
I am trying to use redux state store in a redux app using a rxjs obserable wrapper. Source tutorial
I first tried this approach when I switched from ngrx to redux in angular. Now I’m using this pattern in a react app. However, I have a bit of an issue. When I subscribe to some state store stream I use setState(foo) to store the value in the component. This in turn triggers a new render cycle. I’d like to have one render cycle per component instead of 2 or more.
I’ve been trying to inhibit the initial rendering of the component in order to have it triggered first and only once by the state store subscription. When you have multiple nested components and multiple subscriptions they tend to create wasteful renderings just to do the app init. I know that React does a great job of optimising for multiple renderings but still I find that keeping an eye on the rendering cycles is healthy for avoiding subtle bugs.
Any recommendation on how to trigger the first rendering from the state store subscription?
app.module.tsx
private subscribeToAppSettings() {
DEBUG.cmp && debug('Subscribe appSettings$');
appSettings$().pipe(
skip(1), // For REST api calls I skip the initial state
takeUntil(this.destroyed$),
)
.subscribe(settings => {
DEBUG.subscribe && debug('==> Observe appSettings$', [settings]);
this.setState({ settings });
});
}
As you can see AppModule and everything else is rendered twice because of this subscription. This is a filtered set of logs, showcasing when the app is running the render() methods. Just the init stage, no user interactions.
After reviewing the entire architecture again I figured that I need to manually set the initial state in the components. Now, the initial rendering is doing the useful work, and the second rendering will be ignored by the react change detection.
I still have the extra rendering cycles. However, I see that this is the state of affairs with change detection. A lot of things trigger a second rendering: the init, the router, the event handlers, the observables. As long as React is using the virtual dom for change detection to weed out values that do not actually change, there should be no real impact on performance. As they say: I'm barking at the wrong tree.
state.service.tsx
/** Access state changes as an observable stream */
export const store$ = new Observable<AppState>(observer => {
// All state store observable use `distinctUntilChanged()` operator.
// Without this initial state, `distinctUntilChanged()` will be unable to compare previous and current state.
// As a result, the webapi observable will miss the first response fron the server.
observer.next(appInitialState);
let appState: AppState;
store.subscribe( () => {
appState = store.getState();
observer.next(appState);
});
})
app.module.tsx
constructor(props: any) {
super(props);
DEBUG.construct && debug('Construct AppModule');
this.state = {
navigatorIsVisible: appInitialState.navigator.isVisible,
searchOverlayIsVisible: appInitialState.search.isVisible
} as State;
getAppSettings();
}
search.overlay.smart.tsx
searchOverlayIsVisible$().pipe(
takeUntil(this.destroyed$),
skip(1), // Ignore init state
)
.subscribe(searchOverlayIsVisible => {
DEBUG.subscribe && debug('Observe searchOverlayVisiblity$', searchOverlayIsVisible);
this.setState({ searchOverlayIsVisible });
this.state.searchOverlayIsVisible
});
search.overlay.service.tsx
export function toggleSearchOverlay(isVisible?: boolean) {
if (DEBUG.service && DEBUG.verbose) debug('Toggle search overlay', isVisible);
store.dispatch(
searchActions.toggleSearch(isVisible)
);
return searchOverlayIsVisible$();
}
export const searchOverlayIsVisible$ = () => store$.pipe(
map( state => SEARCH_VISIBILITY(state) ),
distinctUntilChanged()
);
Conclusions
Pushing the initial state in the store$ observable is necessary because we need all the state store observables to recieve their first state. Without this initial state distinctUntilChanged() will not be able to run the comparison between previous and current state. If distictUntilChanged is blocking the obsevables then we end up blocking responses from the webapi. This means we see empty pages even if the state store received the first set of data.
Notice that we are using the component constructor to setup the initial state. Thus, we use the first rendering cycle for useful work. The second rendering will be inhibited by using skip(1) in all state store observables.
Even if we setup init state in constructor we still keep the initial state in reducers as well. All the TOGGLE actions need an initial state to start from.
Be aware that, a lot of processes trigger a second rendering: the init, the router, the event handlers, the observables. As long as React is using the virtual dom for change detection to weed out values that do not actually change, there should be no real impact on DOM rendering performance.
This means it is close to impossible to have just one componentDidUpdate call per route change in LessonsPage. This means we still need to filter out duplicate calls to handlRouteParams().
I have been struggling with this issue for quite some time and have failed to find any answers.
I use react-meteor-data to manage my data with react in my meteor application. It is working fine when dealing with data for mongo but I can't make it reactive with props.
Here in App.js, I call my container which I want to be reactive and rerender when the state of App change.
<MyContainer someState={this.state.MyState} />
In MyContainer.js I have a createContainer from react-meteor-data
export default createContainer(params => {
Meteor.subscribe('someCollection');
return {
someCollection: SomeCollection.find({}).fetch(),
stateFromParent: params.someState
};
}, MyContainer);
This worked fine when rendering the component for the first time, MyContainer correctly get MyState.
The thing is, when the MyState from App change, I can see in Chrome Dev React tool that it is indeed updated for the createContainer( ReactMeteorDataComponent has a prop with the right updated state) but the createContainer function is not run, thus the props do not update for MyContainer.
So the props are updated from ReactMeteorDataComponent but not for MyContainer who keeps indefinitely the data. It's like createContainer doesn't consider the update of its prop has a change and thus doesn't run its function.
I really think I'm missing something since that seems pretty basic stuff, thank you for your help.
The OP did not mention how the state was changed, so the original example is incomplete. Therefore, I will try to explain the gist of how the container creation works, in hope that understanding it will be useful.
How does it work?
It uses meteor's Tracker to auto-update the wrapped component when its computation is invalidated (i.e, when one of the reactive data sources, such as reactive variables, subscription handles or fetched MiniMongo cursors, has a new value). To learn more about Tracker, consult the Tracker manual. This is an in-depth resource, and is not necessary to understand how the basics work.
It does so in a way that is different from the way you normally approach reactivity tracking in Meteor, since it also needs to re-run the computation whenever the container's props are changed.
The source code is not very long or complex and can be found on GitHub (currently here).
Tracker.autorun((c) => {
if (c.firstRun) {
//...
data = component.getMeteorData();
} else {
// Stop this computation instead of using the re-run.
// We use a brand-new autorun for each call to getMeteorData
// to capture dependencies on any reactive data sources that
// are accessed. The reason we can't use a single autorun
// for the lifetime of the component is that Tracker only
// re-runs autoruns at flush time, while we need to be able to
// re-call getMeteorData synchronously whenever we want, e.g.
// from componentWillUpdate.
c.stop();
// Calling forceUpdate() triggers componentWillUpdate which
// recalculates getMeteorData() and re-renders the component.
component.forceUpdate();
}
})
Whenever the computation is invalidated (and therefore rerun), it stops the computation and forces a re-render of the container, which will re-create a new computation and have the updated data.
The high-level container functionality is here (some parts were removed for brevity):
export const ReactMeteorData = {
componentWillMount() {
this.data = {};
this._meteorDataManager = new MeteorDataManager(this); // (1)
const newData = this._meteorDataManager.calculateData(); // (2)
this._meteorDataManager.updateData(newData); // (3)
},
componentWillUpdate(nextProps, nextState) {
// backup current state and props, assign next ones to components
let newData = this._meteorDataManager.calculateData(); // (2)
this._meteorDataManager.updateData(newData); // (3)
// restore backed up data
},
componentWillUnmount() {
this._meteorDataManager.dispose(); // (4)
},
};
The main points are:
- Before being mounted, a new data manager is created (1). It is in charge of running the computation and populating this.data according to data changes.
- At first and whenever the component should update, the computation is run (2) and the data is updated (3). The update happens whenever the component receives new state or props (in this type of container, it should only be props), and, as we saw earlier, also when the Tracker computation is invalidated, due to the call to component.forceUpdate().
The wrapped component receives the parent's props, as well as the Tracker computation's data as props:
return <WrappedComponent {...this.props} {...this.data} />;
Any more points as to how it should be used?
The react-meteor-data has a short section in the meteor guide.
Generally, the simple example in the guide (as well as the OP's example) should work just fine, as long as the state is set appropriately, using setState() (see the "how does it work?" section above).
Also, there is no need to re-map the container state to props sent to the child, as they are passed along (unless there is a very good reason for doing so).
Do consider the point in the preventing re-renders section if you encounter any performance issues.
From the guide:
export default ListPageContainer = withTracker(({ id }) => {
const handle = Meteor.subscribe('todos.inList', id);
const loading = !handle.ready();
const list = Lists.findOne(id);
const listExists = !loading && !!list;
return {
loading,
list,
listExists,
todos: listExists ? list.todos().fetch() : [],
};
})(ListPage);
in this example, note that the container expects an id prop, and it will also be made available to the wrapped component, as well as loading, list, etc (which come from the container's computation in the example).