withState accepts a function to return default state:
withState('show', 'setShow', ({ showInitally }) => showInitally);
so if the resulting component is called with <MyComponent showInitally />
the show state will be true.
However, if props get updated (showInitally changes to false), the state will not update.
How can you achieve that state changes when props change?
For logic where state could have a multiple sources of true I highly recommend you to use
mapPropsStream enhancer, with rxjs or any other library.
Or to write your own enhancer.
In most cases multiple sources of one state is a bad idea, but yes there are few situations we could need it.
I used such logic a lot in the past, but now every time I see the need of updating internal state based on external properties, I try to find a solution how to rewrite code and get just one source of true.
I'll try explain shortly the problems I got trying to write universal solution of issue above.
What if internal state was changed inside component,
then component props changed, do I need to reset state or not.
Do I need to reset state if some props changed, or on every render.
How I could reset state to initial value if props it depends were not changed.
How I could to combine current state with props on reset.
I tried a lot of solutions, and all of them had issues, I even wrote issues about https://github.com/acdlite/recompose/issues/199
And be sure example in #199 has bugs.
May be a good abstraction exists, I don't know.
For me the simple solution now just avoid such behaviour or write such logic with rxjs.
I am agree with @istarkov's suggestion. However, if what you need is a quick hack, you can change key to force React to remount the component and re-evaluate the initial state. Ex:
<MyComponent showInitally key={showInitally} />
Please know that this approach is a hack and should be used with caution.
@wuct may be not a hack as sometimes it's needed to force creating a new DOM element.
For example I use it to force creating new img element on src change <img src={src} key={src} /> Because otherwise until new image loaded you will see previous image, and in a lot of cases it's not expected behaviour.
<img src={src} key={src} /> is a great use case!
Hi Guys,
thanks for your answers!
Maybe some more background infos: i use withState together with mantra specification: https://github.com/kadirahq/mantra in a meteor-app.
mantra also uses react-komposer which also uses HOCs to compose containers. See a sample for meteor here: https://github.com/kadirahq/react-komposer#using-with-meteor
In the sample above onData is called with new "posts" whenever posts change. posts is a property that gets passed to the next component. So you can do something like this (slightly altered sample):
function composer({postId}, onData) {
if (Meteor.subscribe('posts.one', postId).ready()) {
const post = Posts.findOne(postId);
onData(null, {post});
};
};
composeAll(
// composeAll is from react-komposer and is similar i think to recompose' compose
// but it wraps from "bottom to top"
withState("highlightPost", "setHighlighPost", ({post}) => post.highlightedInitially),
composeWithTracker(composer)
)
So in this arbitrary example, a post can be highlighted either by specifing it on the post-document (from Posts.findOne(postId);) or by the state (maybe from a button on the wrapped componen (not very useful, but you get the idea).
It's indeed not obvious, which is the "truth", as @istarkov stated. In this case, the collection should always win. So if it changes its props, it would reset the state (state "highlightPost").
In my case I have a similar, but slightly more complicated example with draftjs (https://github.com/facebook/draft-js):
with draftjs, you need to keep the current editorState in a state. I save the state into a meteor-collection when a save button is pressed. Initially, the state should be populated from the meteor-collection. And also if the collection has been changed (this means another person has changed the content of the editor).
I found a dirty workaround for my case:
export default composeAll(
// this here is a dirty workaround
withPropsOnChange(['initialEditorState'], ({ initialEditorState, setEditorState }) => {
// initialEditorstate has been changed, now change the editor State
setEditorState(initialEditorState);
}),
withState('editorState', 'setEditorState', ({ initialEditorState }) => initialEditorState),
composeWithTracker(composer),
useDeps(depsMapper)
)(Editable);
export const composer = ({ context, contentId }, onData) => {
const { Meteor, Collections, i18n } = context();
if (Meteor.subscribe('contents.one', contentId).ready()) {
const locale = i18n.getLocale();
const content = Collections.Contents.findOne(contentId);
const initialEditorState = editorStateFromRaw(content ? content[`value_${locale}`] : null);
onData(null, { content, locale, initialEditorState });
}
};
This workaround works, but throws a warning sometimes:
Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
mapPropsStream will solve your problem and all others ;-)
pseudocode:
mapPropsStream(props$ => {
const { handler: setEditorState, stream: editorState$ } = createEventHandler();
const combinedState$ = Observable.merge(
editorState$,
props$.map(({ initialEditorState }) => initialEditorState).distinctUntilChanged()
).startsWith('DEFAULT EDITOR STATE'); // depends want your or not to wait initialState from db
return props$.combineLatest(combinedState$, (props, editorState) => ({
...props,
editorState,
setEditorState,
});
})
I used in the past my own version of something like composeWithTracker to work with Meteor, but used that outside components as redux middleware (simple actions like subscribe, unsubscribe is an input and update actions for redux state is an output),
and used the state from redux. Such solution is also will solve your problem as the only editorState will be the state in redux.
Here is how i do with meteor
const enhance = compose(
connect(mapStateToProps, mapDispatchToActions),
lifecycle({
componentDidMount: function() {
logger.debug("Component mounted", this.props);
this.tracker = Tracker.autorun(() => {
if(Meteor.subscribe('boxes.list').ready())
this.props.syncBoxes()
})
},
componentWillUnMount: function() {
this.tracker.stop();
}
})
)
export default enhance(Dashboard)
I am going to close this issue because it's no longer active. Please feel free to reopen it if you have further input.
Most helpful comment
@wuct may be not a hack as sometimes it's needed to force creating a new DOM element.
For example I use it to force creating new img element on src change
<img src={src} key={src} />Because otherwise until new image loaded you will see previous image, and in a lot of cases it's not expected behaviour.