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).
Related
React collects operations(like DOM operations such 'ADD','REPLACE','REMOVE' and more) so they could execute them in batch in one shot at the end of each render.
for example, setState call inside React component is scheduled to the end of the construction of this React tree by adding this operation to the operation queue. then react will go over this queue and will decide what changes need to be made to the DOM.
React will decide whether or not to call another render based on whether or not the operation queue is empty or not.
more info on this awesome tutorial which summarizes the basics of how React works internally.
I need access to this queue to decide whether or not the current render was the last for a very custom React component. (YES maybe I can avoid it but currently, this is my requirement)
the access must be from inside of this component.
my check will be called from the lastest useEffect which is after the render ends and the DOM was updated and is the latest lifecycle event, so if the operation queue is empty there will be no more renders for sure. (here nice article explains and demonstrates the order of hook calls)
couldn't find any public API, but a workaround would also be acceptable. (forking and editing React is not a workaround)
this src file is probably the main logic to this queue. and this is the type of the actual queue. however this is the source code and this queue is not exported in the built versions of react(neither development or production build)
so, is there a way to access the internal operation queue of React?
EDIT - Warning
this is for educational purposes only - do not use it on production!
this approach is not safe based on React core team member - I've already asked. it could be safe if you plan to use a fixed version of React without upgrading later.
####### END OF EDIT #######
SOLVED!
so after many many hours digging into React codebase I finally wrote a hook that tells if any update is currently scheduled.
Note: would work for function components only, and this hook is not well tested.
you can see some internal state of React by the undocumented __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED property. this prop holds ReactCurrentOwner which is basically a reference to the current component that is being constructed.
const currentOwner = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner?.current;
currentOwner is the current component that is being constructed. this prop is available during renders only(because in effects is after render no component is being currently constructed).
but because another render can be triggered from state set from effects we should always call ower check from the latest effect
inside it .current.memoizedProps you will find a linked list of all hooks that were declared to this point on this component.
each hook holds a queue for holding scheduled updates, and inside it, there is a pending prop that tells if any update is currently scheduled for the next render.
we could run over this linked list to find out if an update is scheduled by any hook:
const wouldUpdate = (currentOwner) => {
let newObj = currentOwner?.memoizedState;
// go over the linked list of hooks and find out if there is any pending update
while (newObj && 'next' in newObj) {
newObj = newObj['next'];
if (newObj?.queue?.pending) return true;
}
return false;
};
so to summer up we could build a custom hook to check if the current render is the latest scheduled render:
const wouldUpdate = (currentOwner) => {
let newObj = currentOwner?.memoizedState;
// go over the linked list of hooks and find out if there is any pending update
while (newObj && 'next' in newObj) {
newObj = newObj['next'];
if (newObj?.queue?.pending) return true;
}
return false;
};
export const useDoesUpdateIsScheduled = () => {
// #ts-ignore
// hold the current owner ref so we could call it from effects
const currentOwner = useRef(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner?.current);
return () => wouldUpdate(currentOwner.current);
};
so many hours for so little code... ;<
Usage:
const YourComponent = (props) => {
//..
// code, hooks ,logic, effects would be here
//..
// should be could from the last useEffect
const wouldUpdate = useDoesUpdateIsScheduled();
useEffect(() => {
console.log(wouldUpdate());
});
return <div>... your jsx here ...</div>;
};
screenshot of test component on mount:
you can see that on the latest render our hook tells us there are no pending updates.
you can also call wouldUpdate from function body but take into account that updates can be scheduled from effects (means that calling it while rendering would not catch these updates)
the popular why-did-you-render also uses this undocumented __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED property to achieve it's goal.
THATS IT(actually it wasn't worth the hours, and it will probably break on various cases as this is not public API).
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 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().
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?
I have the following code in my render method:
render() {
return (
<div>
{this.props.spatulaReady.ready() ? <p>{this.props.spatula.name}</p> : <p>loading spatula</p>}
</div>
)
}
Which according to my understanding, checks if the subscriptionhandle is ready (data is there) and displays it. If no data is available, it should display a simple loading message. However, when I first load the page this snippet is on, it get's stuck on the loading part. On a page reload the data (usually) displays fine.
If I check the spatulaReady.ready() when the page first loads and while the display is stuck on 'loading spatula', and the data that should be there, the handle reports as ready and the data is there like it is supposed to be. If I refresh the page it all displays fine as well. The problem is, this way of checking for data and rendering if it has arrived has worked fine for me in the past. Is it because the render method is not reactive? Because handle.ready() should be reactive.
What makes it even weirder is that it sometimes DOES correctly display the data on page load, seemingly at random.
CreateContainer code:
export default createContainer(props => {
return {
user: Meteor.user(),
spatulaReady: Meteor.subscribe('spatula.byId', props.deviceId),
spatula: SpatulaCollection.findOne()
}
}, SpatulaConfig)
Publication code:
Meteor.publish('spatula.byId', function(deviceId) {
const ERROR_MESSAGE = 'Error while obtaining spatula by id'
if (!this.userId) //Check for login
throw new Meteor.Error('Subscription denied!')
const spatula = SpatulaCollection.findOne({_id: deviceId})
if(!spatula) //No spatula by this id
throw new Meteor.Error(403, ERROR_MESSAGE)
if(spatula.ownedBy != this.userId) //Spatula does not belong to this user
throw new Meteor.Error(403, ERROR_MESSAGE)
return SpatulaCollection.find({_id: deviceId})
})
I know I'm missing a piece of the puzzle here, but I've been unsuccessful at finding it. If you don't know the solution to my specific problem, pointing me in the right direction with another way of waiting for the data to arrive before displaying it is also greatly appreciated.
EDIT: After doing some trial-and-error and reading various other posts somewhat related to my project, I figured out the solution:
export default createContainer(props => {
const sHandle= Meteor.subscribe('spatula.byId', props.deviceId)
return {
user: Meteor.user(),
spatulaReady: sHandle.ready(),
spatula: SpatulaCollection.findOne()
}
}, SpatulaConfig)
It still makes no sense to me that moving the ready() call to create container fixed all my problems though.
As you figured out, moving the .ready() call to createContainer fixes the problem. This is because Meteor reactivity only works when you call a reactive data source (a reactive function), such as collection.find() or subscriptionHandle.ready() within a reactive context, such as Tracker.autorun or createContainer. Functions within the React component, including render, are not reactive contexts from Meteor's perspective.
Note that React and Meteor reactivity are two different things. React's reactivity works simply so that whenever a component's props or state change, it's render function is re-run. It does not understand anything about Meteor's reactive data sources. Since createContainer (that is re-run by Meteor reactivity when reactive data sources in it change) simply passes props to the underlying component, the component is re-rendered by React when the props given from createContainer change.