mobxjs / mobx-react

React bindings for MobX
https://mobx.js.org/react-integration.html
MIT License
4.85k stars 350 forks source link

mobx-react 6 #640

Closed mweststrate closed 5 years ago

mweststrate commented 5 years ago

Things to be addressed:

mweststrate commented 5 years ago

To summarize (and check if I remember the setup correctly)

  1. An observer component will always pass a new closure to useObserver each time it renders. Without this, changes in the props wouldn't be picked up*.
  2. An observer component is memo, because otherwise it would always rerender if a parent rerenders, and that rerender would always cause the useObserver to re-render as well (due to the new closure). In other words, if it wasn't a memo, then toggling an observable value in the root component would cause the entire component tree to re-render.

N.B. theoretically <Observer> could be memoized as well, as it might receive the same closure over and over again (this happens if observer is used on a class component for example). But since observer itself is already memo, there is no real need; if the class component has a change in state / props, it will re-render anyway.

urugator commented 5 years ago

I think that's correct. In theory we could have useObserver(render, [prop1, prop2]) returning previously rendered element directly if prop1/prop2 haven't changed and reaction isn't dirty... But closure is always recreated, that's the property of hooks (supposedly without significant impact).

jeremy-coleman commented 5 years ago

this link is to a very small wrapper to use hooks with component classes https://github.com/kesne/with-react-hooks/blob/master/src/index.tsx

it may be useful to use something similar to unify the interface of observer((props) => , either as as like 6.0.0.bridge or even as a permanent solution.

idk if this has been considered or not, but its a pattern i've been using recently to refactor from component classes

ex: this is after a refactor


//this isnt a react anything even though the content field has react related data
class HostLinkValue {
    href: any;
    content: any;
    _handleClick: (e: any) => void;

    constructor(props){
        this._handleClick = (e) => {
            e.preventDefault();
            props.onClick && props.onClick(),
            props.host.open && props.host.open(props.request).then(openedHost => {
                if(props.onHostOpened) {props.onHostOpened(openedHost) }
            }),
            props.host.load && props.host.load(props.request)
        }
        this.href = props.host.getUrl(props.request);
        this.content = React.Children.count(props.children) > 0 ? props.children : props.title
    }

    @action
    click = e => this._handleClick(e)
}

export const AppLink = observer((props) => {
    const link = new HostLinkValue(props)
        return (
            <a 
            style={{color: 'blue'}}
            className={props.className}
            title={props.title}
            href={link.href}
            onClick={link.click}>{link.content}
            </a>
        );
    }
)

using this pattern I can move pretty much everything except lifecycle hooks outside of react , which for me is a win - and if used with the component wrapper above pretty much covers everything with minimal effort.

maybe instantiating the stateful class outside react then passing it as a parameter to useEffect wouldn't require any observer wrapper at all?

no idea if this would work , seems like it would though lol - useEffect is basically autorun right? and the second param is like only update for keys

import {useEffect} from 'react'

export const AppLink2 = (props) => {
var link = new HostLinkValue(props)
    useEffect(() => void 0, [link])
        return (
            <a 
            style={{color: 'blue'}}
            className={props.className}
            title={props.title}
            href={link.href}
            onClick={link.click}>{link.content}
            </a>
        );
}
jeremy-coleman commented 5 years ago

Tldr, is mobx react even needed now? React has the mechanics to handle reactions to data.

The biggest thing to keep is inject , but maybe it should move to the mobx library now and just inject inside the constructor?

danielkcz commented 5 years ago

React has the mechanics to handle reactions to data.

Care to elaborate on what mechanics it has? If you are pointing out to Context, that's far from ideal as it will always re-render a whole tree starting at the Provider.

The biggest thing to keep is inject , but maybe it should move to the mobx library now and just inject inside the constructor?

The biggest? :) On the contrary, it got super easy with Context to have mobx stores around.

https://github.com/mobxjs/mobx-react-lite#why-no-providerinject

jeremy-coleman commented 5 years ago

1) useEffect is very similar to autorun - callbacks on changed data, specific property watchers, it returns a disposer. pretty much the same. It rerenders the component on data changes. As long as the data changes are efficient (via mobx) the renders should be too, right? you can also use it in conjunction with useRef for any object not just dom elements.

1.1) the one thing that useEffect doesn't do it is provide a mechanism to directly call forceUpdate - maybe they were worried about self-referencing effects or something? who knows. I think the current workaround just uses an empty useState call, but maybe a similar behavior could come from using a dummy variable on the object you pass into useEffect?

2) dependency injection isn't limited to react components nor is mobx limited to react - imo one of the best things about mobx is you can easily share stores between rendering libraries. I also don't really think context is an ideal use for mobx stores now with the other hooks available.

I think at its core, mobx is designed to efficiently and reactively update data. react now has a set of reactive hooks to run updates on the views after data changes

i guess I don't understand what the need is now?

devuxer commented 5 years ago

The docs say:

Subscribe to this issue for a proper migration guide.

What I see here is long discussion about the future of mobx-react. As interesting as this is, where do I go to learn how to use mobx with functional (instead of class) components? What does useObservable do that useState doesn't? When/how do I use @action or action or runInAction? The API is laid out pretty well in the docs, but I'm a little confused which circumstances to use mobx vs. just plain react state.

danielkcz commented 5 years ago

@devuxer Yea, migration guide is still kinda in the wind, sorry about that :)

Regarding your state questions have a look at https://github.com/mobxjs/mobx-react-lite/issues/69

If you have further questions regarding the use of mobx in functional components, feel free to open issue at mobx-react-lite repo.

mweststrate commented 5 years ago

@Jeremy I think the big point you are missing is that React can do all these things only with the state that is owned by the same component. However, you cannot run a useEffect etc etc on state that doesn't live in the component. So fundamentally nothing really changed, for purely local state, one could always already use setState + lifecycle hooks. That changed now to hooks / effects, but it still the same scope. The real value of mobx kicks in as soon as you have state stored outside the component / in different components, or complex derivations come into play.

On Sat, Mar 9, 2019 at 8:41 PM Daniel K. notifications@github.com wrote:

@devuxer https://github.com/devuxer Yea, migration guide is still kinda in the wind, sorry about that :)

Regarding your state questions have a look at mobxjs/mobx-react-lite#69 https://github.com/mobxjs/mobx-react-lite/issues/69

If you have further questions regarding the use of mobx in functional components, feel free to open issue at mobx-react-lite https://github.com/mobxjs/mobx-react-lite repo.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-react/issues/640#issuecomment-471215319, or mute the thread https://github.com/notifications/unsubscribe-auth/ABvGhPH1tAgwh5bEU7empcUwvwKwPWOiks5vVA5qgaJpZM4amxi7 .

jeremy commented 5 years ago

Wrong @jeremy, FYI. On Tue, Mar 12, 2019 at 09:05 Michel Weststrate notifications@github.com wrote:

@Jeremy I think the big point you are missing is that React can do all these things only with the state that is owned by the same component. However, you cannot run a useEffect etc etc on state that doesn't live in the component. So fundamentally nothing really changed, for purely local state, one could always already use setState + lifecycle hooks. That changed now to hooks / effects, but it still the same scope. The real value of mobx kicks in as soon as you have state stored outside the component / in different components, or complex derivations come into play.

On Sat, Mar 9, 2019 at 8:41 PM Daniel K. notifications@github.com wrote:

@devuxer https://github.com/devuxer Yea, migration guide is still kinda in the wind, sorry about that :)

Regarding your state questions have a look at mobxjs/mobx-react-lite#69 https://github.com/mobxjs/mobx-react-lite/issues/69

If you have further questions regarding the use of mobx in functional components, feel free to open issue at mobx-react-lite https://github.com/mobxjs/mobx-react-lite repo.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <https://github.com/mobxjs/mobx-react/issues/640#issuecomment-471215319 , or mute the thread < https://github.com/notifications/unsubscribe-auth/ABvGhPH1tAgwh5bEU7empcUwvwKwPWOiks5vVA5qgaJpZM4amxi7

.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-react/issues/640#issuecomment-472060693, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAAx9R56zU0c4UHH_wS8v42Cy3ViWuRks5vV9BegaJpZM4amxi7 .

jeremy-coleman commented 5 years ago

ah, thanks michel, i was under the impression useRef had changed to let you reference outside objects not just dom components, but... very simple example here doesn't work :\

import * as React from "react";
import {observable, action} from 'mobx'
import {useRef, useEffect, useCallback, useState} from 'react'

class CounterStore {
  @observable count
  constructor(){this.count = 1}
  @action inc = () => this.count ++ && console.log('got inc', this.count)
  @action dec = () => this.count --
}
const counterStore = new CounterStore()

const MobxCounter = () => {
  var counter = useRef(counterStore)
  return (
    <div className="counter">
    <p>Mobx: You clicked {counter.current.count} times</p>
    <button onClick={counter.current.inc}>Click me</button> 
    </div>
  );
};

//also doesnt work
function MobxCounter2(){
  var counter = () => useRef(counterStore)
  return (
    <div className="counter">
    <p>Mobx: You clicked {counter().current.count} times</p>
    <button onClick={counter().current.inc}>Click me</button> 
    </div>
  );
};
danielkcz commented 5 years ago

@jeremy-coleman You can store objects in useRef, but they are not reactive in any way on its own.

Any component can basically re-render itself only with useState or useReducer hooks. The useEffect you talked about before (but not using in the example) cannot do much on its own, it's for side-effects that can be run based on variable changes. You should probably read Hooks docs more throughly, seems you are missing basics :)

MobX for React is not some magic box either. It utilizes forceUpdate (in classes) or useState (in fc) to re-render the component when it sees a change to an observable variable. And that's the role of observer which can track such observable variables. Modify example like this and it will work just fine.

const MobxCounter = () => {
  var counter = useRef(counterStore)
  return useObserver(() => (
    <div className="counter">
    <p>Mobx: You clicked {counter.current.count} times</p>
    <button onClick={counter.current.inc}>Click me</button> 
    </div>
  ));
};

Your second example is weird. For every call of counter() you would get another ref, so there would be two refs with the exact same reference to the same store. It would work with useObserver, but I don't really recommend doing it like that.

jeremy-coleman commented 5 years ago

thanks for the example freddy, i know the code i posted wasn't proper , I tried about every combination of useEffect , useRef and useState but the view would never update. react docs seem misleading saying: The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. (aka an injected mobx store) and even in the effect example.

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

I realized later it's probably due to the combination of react trying to be lazy causing mobx to not see the value as being observed

danielkcz commented 5 years ago

@jeremy-coleman This is very offtopic, but where do you see "misleading part"? It is true that you can store anything to the ref, but it won't re-render component when you change the value.

The useEffect will execute when it's dependencies change, but it's not reactive. The component itself must re-render first for the useEffect to be evaluated again and compare dependencies to decide if contained side effect should be executed. The useEffect is not meant as a primary way to re-render component unless you change the state.

Please, if you have further questions, use either https://gitter.im/mobxjs/mobx or https://spectrum.chat/mobx-state-tree or even better https://spectrum.chat/react

jeremy-coleman commented 5 years ago

from the react docs: 1)"This makes it (useEffect) suitable for the many common side effects, like setting up subscriptions and event handlers" 2)"By default, effects run after every completed render, but you can choose to fire it only when certain values have changed". I mean they literally use a subscription in the example. i understand why it doesn't work though now

mweststrate commented 5 years ago

Discussing either useEffect or React versus MobX in general is totally offtopic in this thread. It is long enough as it is without that. Please continue this discussion elsewhere :)

mayorovp commented 5 years ago

@urugator

It also makes mobxjs/mobx#1811 impossible to implement, because we don't have an access to reaction in SCU, correct?

Observer implementation still have access to it's own reaction.

urugator commented 5 years ago

@mayorovp Just to clarify. I meant this impl https://github.com/mobxjs/mobx-react/blob/v6-radical/src/observer.js It uses <Observer> respectively useObserver hook, which holds the reaction reference ... so the hook has to somehow expose the reaction to the parent class based component with the access to SCU. Perhas it's easily doable, I don't know, it just occured to me, because the reaction is no longer created by class component...

mweststrate commented 5 years ago

mobx-react@6.0.0-rc.2 is now available!

Could someone give it a quick try on react-native?

danielkcz commented 5 years ago

@mweststrate I am curious why have you decided to keep JavaScript code and have a separate typings? Is there some benefit I am not seeing? It feels strange considering that mobx and mobx-state-tree are both full TypeScript.

Point is that I am mostly getting used to the idea that mobx-react-lite will eventually become obsolete an part of this module. However, I would definitely like to keep TypeScript in the code and that makes merging those together rather problematic.

mweststrate commented 5 years ago

No, just too lazy to migrate 😊

Op vr 22 mrt. 2019 18:40 schreef Daniel K. notifications@github.com:

@mweststrate https://github.com/mweststrate I am curious why have you decided to keep JavaScript code and have a separate typings. Is there some benefit I am not seeing?

Point is that I am mostly getting used to the idea that mobx-react-lite will eventually become obsolete an part of this module. However, I would definitely like to keep TypeScript in the code and that makes merging those together rather problematic.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-react/issues/640#issuecomment-475714193, or mute the thread https://github.com/notifications/unsubscribe-auth/ABvGhPd_KmdI8oYyl0Qva_jBjP-BHxelks5vZRWmgaJpZM4amxi7 .

danielkcz commented 5 years ago

Ok, I guess we can migrate it after V6 comes out, it's not going to be breaking change or anything. I am willing to help with that eventually.

danielkcz commented 5 years ago

Btw, about the auto-memo discussion above. I actually got burned by that. There is a problem with the Context. If its value changes, the tree under the Provider gets re-rendered. The memo will serve as a bouncer of such change. No component below the observer would be re-rendered because props did not change. I had to switch to useObserver after some hour of being annoyed what's happening.

The obvious advice is to have an observable in the Context, but this more of the legacy code concern which uses old react-form package. I assume there are still other valid libraries with such approach that people would use and they would get burned by it.

I am not sure if it's enough for getting rid of the memo, but it's not ideal in its current form either.

urugator commented 5 years ago

@FredyC Can you share an example? memo doesn't block context changes. The legacy context updates are broken and de facto forbidden

danielkcz commented 5 years ago

@urugator You are right, seems the issue of the old context actually. I guess I have to get rid of that legacy react-form in a first place :) False alarm then, at ease :)

nareshbhatia commented 5 years ago

@mweststrate & @FredyC, I tried out mobx-react@6.0.0-rc.4 today. Everything works fine, but I fail to see where I could use any hooks ported from mobx-react-lite (it is not clear what was carried over). In fact, I found the use of <Observer> also very awkward. Details below refer to my GitHub repo. Please let me know what I am missing and how I can better leverage the new functionality.

Please let me know how this code can benefit from v6 features. TIA.

danielkcz commented 5 years ago

@nareshbhatia Seems like you are a bit confused. V6 does not really bring any new essential features on the table. It's pretty much a starting point where class and functional (with hooks) components are supported in a single package. It uses mobx-react-lite underneath for a hooks support with a fallback to "old behavior" to support classes.

You can just keep using observer HOC if you like or useObserver hook if that's more up to your taste. The <Observer> is surely awkward to use once someone gets a taste of hooks, so unless you like it, you don't need to use it.

I had no time to analyze your use case, just know that major breaking change is a need for React 16.8. Everything else should be backward compatible. And some deprecated stuff got removed, but that's normal.

nareshbhatia commented 5 years ago

@FredyC, thanks for the clarification on the purpose of V6. This is such a long thread that I could not easily locate it.

In any case, the purpose of my repo is to demonstrate the latest best practices in the React ecosystem. Hence I am using function components everywhere and hooks wherever they makes sense (useContext, useEffect, useStyles). If you don't have much time to review in detail, please just look at AccountPanel. I think this a case where the observer() hoc ends up in smaller code than using useObserver() twice. TIA.

Keats commented 5 years ago

@mweststrate I've tried the latest v6 rc.4 (with mobx 4) on a NextJS project that requires some IE11 support and it is not working anymore in some cases. The issue I had was that one page was not firing any onClick events in IE11. I tracked down the issue to an inject: removing it made the clicks fire again. I have absolutely no clue why this page in particular was behaving this way considering I have other pages with inject working perfectly fine. Downgrading to 5.4.3 solved the issue.

The smallest reduction I did was:

@inject(({ rootStore }) => ({
  annotationStore: rootStore.annotationStore
}))
class MyPage extends React.PureComponent<{}> {
  render() {
    return <button onClick={()=> console.log("clicked")}>Click me</button>;
  }
}

Again, the same code worked for other pages so I am not really sure what is the root cause there but downgrading does fix the issue.

danielkcz commented 5 years ago

@Keats It would really help if you would manage to put together working reproduction, ideally in https://codesandbox.io. Otherwise, we are probably clueless as much as you are if it's happening only in a specific case in your app.

Keats commented 5 years ago

I spent about 30 minutes trying to reproduce it in codesandbox with no luck :/ I'll post it here if I manage to do so

keithort commented 5 years ago

I have 6.0.0-rc.4 running on a couple internal apps and things seem to be solid enough. Many components were migrated from another application and converted from class to functional components pretty seamlessly. I do think there is room for improvement on the documentation but overall I am happy. Thanks for the hard work.

asaarnak commented 5 years ago

Great thread, read the whole thread. But didn't find if there will be IE11 supported version for hooks? Unfortunately our client has many other old applications and new applications must support IE11.

mweststrate commented 5 years ago

Hooks and IE11 are unrelated, and IE 11 is supported as long as you stick to mobx(!) 4, which will be compatible with mobx-react 6

Op vr 5 apr. 2019 10:24 schreef asaarnak notifications@github.com:

Great thread, read the whole thread. But didn't find if there will be IE11 supported version for hooks? Unfortunately our client has many other old applications and new applications must support IE11.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-react/issues/640#issuecomment-480175227, or mute the thread https://github.com/notifications/unsubscribe-auth/ABvGhLIJ63QTTlnh2VOSmJsnr2m7CLujks5vdvodgaJpZM4amxi7 .

asaarnak commented 5 years ago

If i stick to mobx@4 will new features described here be in mobx@4 ?

danielkcz commented 5 years ago

@asaarnak Which part of this sentence you did not understand? :)

mobx(!) 4, which will be compatible with mobx-react 6

asaarnak commented 5 years ago

Sorry, i see now. :)

mweststrate commented 5 years ago

N.B. release has been postponed a bit, until we've found the most optimal API for combining observables with hooks. Which is not really a technical problem, but we want to make sure there aren't to much caveats or confusing variations in the api. Feel free to chime in! https://github.com/mobxjs/mobx-react-lite/issues/94

In the mean time, if you are still on mobx-react@5, just continue happily throwing in <Observer> into your hook based components :)

chadmorrow commented 5 years ago

Thanks for the update! Really looking forward to the release. Been using the beta version for a while and it hasn't caused any problems. Only annoyance with hooks is the inability to see values for them in dev tools but that's obviously not a mobx issue.

nghiepdev commented 5 years ago

Hi all, I have mobx-react@6.0.0-rc.4 and Next.js@8

I don't use both @inject() and static getInitialProps at the same time.

getInitialProps don't execute when @inject has been used.

danielkcz commented 5 years ago

@nghiepit Um, what is getInialProps? Do you mean getDefaultProps or getInitialState? Can you provide reproduction in either repo or CodeSandbox showing the problem?

mweststrate commented 5 years ago

In any case, best report problems in a separate issue, including the normally required information such as a reproduction

On Tue, Apr 30, 2019 at 11:12 AM Daniel K. notifications@github.com wrote:

@nghiepit https://github.com/nghiepit Um, what is getInialProps? Don't you mean getDefaultProps? Or getInitialState? Can you provide reproduction in either repo or CodeSandbox showing the problem?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-react/issues/640#issuecomment-487874602, or mute the thread https://github.com/notifications/unsubscribe-auth/AAN4NBAXLSXSRHKKN4LZVUTPTAERHANCNFSM4GU3DC5Q .

nghiepdev commented 5 years ago

@FredyC @mweststrate My CodeSandbox https://codesandbox.io/embed/github/nghiepit/next-mobx-bug-report/tree/master/?fontsize=14&module=%2Fpages%2Fother.js

danielkcz commented 5 years ago

@nghiepit There is no known getInitialProps static method in React, you probably mean getDefaultProps. You got confused there. Please if you have further questions do as @mweststrate said, open the new issue with all relevant information.

urugator commented 5 years ago

@nghiepit For whatever reason getInitialProps doesn't seem to be hoisted, so you have to define the function on exported Injector: https://codesandbox.io/s/2o08jqxlnp Please create a seperate issue if you have further questions. EDIT: I see, you're using v6, which no longer hoists statics (it's actually mentioned in the first comment...)

nghiepdev commented 5 years ago

@FredyC I'm using Next.js and getInitialProps is the helpfull function in Next.js Thanks to @urugator I will temporarily hot-fix like you. Hope, It will fix soon.

mweststrate commented 5 years ago

@nghiepit so far there are no plans to explicitly hoist statics (it would be could to mention the above work-arounds in the docs). If that is a problem and you would like to challenge it, please open a separate issue, inside this conversation the problem / discussion will get lost

simo-eskalera commented 5 years ago

@mweststrate I experimented with a simple context based hook that fulfills the role of the current inject functionality and used it along side the <Observer> component from mobx-react (not *-lite). However, it's not not picking up state updates:

export const StoreContext = React.createContext({});
export function useStore(mapActions) {
  const globalStore = useContext(StoreContext);
  let store;
  if (typeof mapActions == 'string') {
    store = globalStore[mapActions];
  } else {
    store = mapActions(globalStore);
  }
  return store; // I also tried `return observer(store)`
}

Usage as follows: Injecting the Context into the app

import * as stores from '../stores'; // imports all instances of mobx stores
<StoreContext.Provider value={stores}>
      {children}
</StoreContext.Provider>

Sample Component showing usage:

const MyComponent = () => {
  const profileStore = useStore(stores => stores.profileStore);
  const { profile } = profileStore;
  return <Observer>
    {() => (<div>Name: {profile.name}</div>)}
  </Observer>
};

So I'm wondering if inject has some magic behavior under the hood other than just injecting stores that my useStore implementation is missing.

urugator commented 5 years ago

@simo-eskalera

it's not not picking up state updates

Updates of what exactly? All observables must be accessed from within observer/Observer/useObserver. So the modifications of stores.profileStore and profileStore.profile won't be picked, only profile.name will.

simo-eskalera commented 5 years ago

@urugator nvm, I wrote a demo and it worked fine there: https://codesandbox.io/s/w7y0180w0k

I wonder if we have an equivalent to useStore, like the one I have in the demo link. Usage makes it easy to immediately understand what you're getting. Here's some flavors:

  // Specified as a string
  const timerStore = useStore("timerStore");

  // Specified as a function
  const timerStore = useStore(stores => stores.timerStore); 

  // deconstruction replaces direct injection of store props via `inject(mapToProps)` function
  const { timer, reset } = useStore("timerStore");