Git Product home page Git Product logo

mobx-react-lite's Introduction

mobx-react-lite


🚨🚨🚨 This repo has been moved to mobx


CircleCICoverage StatusNPM downloadsMinzipped size

TypeScriptcode style: prettier

Discuss on Github View changelog

This is a lighter version of mobx-react which supports React functional components only and as such makes the library slightly faster and smaller (only 1.5kB gzipped). Note however that it is possible to use <Observer> inside the render of class components. Unlike mobx-react, it doesn't Provider/inject, as useContext can be used instead.

Compatibility table (major versions)

mobx mobx-react-lite Browser
6 3 Modern browsers (IE 11+ in compatibility mode)
5 2 Modern browsers
4 2 IE 11+, RN w/o Proxy support

mobx-react-lite requires React 16.8 or higher.


API reference βš’

observer<P>(baseComponent: FunctionComponent<P>): FunctionComponent<P>

The observer converts a component into a reactive component, which tracks which observables are used automatically and re-renders the component when one of these values changes. Can only be used for function components. For class component support see the mobx-react package.

<Observer>{renderFn}</Observer>

Is a React component, which applies observer to an anonymous region in your component. <Observer> can be used both inside class and function components.

useLocalObservable<T>(initializer: () => T, annotations?: AnnotationsMap<T>): T

Creates an observable object with the given properties, methods and computed values.

Note that computed values cannot directly depend on non-observable values, but only on observable values, so it might be needed to sync properties into the observable using useEffect (see the example below at useAsObservableSource).

useLocalObservable is a short-hand for:

const [state] = useState(() => observable(initializer(), annotations, { autoBind: true }))

enableStaticRendering(enable: true)

Call enableStaticRendering(true) when running in an SSR environment, in which observer wrapped components should never re-render, but cleanup after the first rendering automatically. Use isUsingStaticRendering() to inspect the current setting.


Deprecated APIs

useObserver<T>(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T (deprecated)

This API is deprecated in 3.*. It is often used wrong (e.g. to select data rather than for rendering, and <Observer> better decouples the rendering from the component updates

interface IUseObserverOptions {
    // optional custom hook that should make a component re-render (or not) upon changes
    // Supported in 2.x only
    useForceUpdate: () => () => void
}

It allows you to use an observer like behaviour, but still allowing you to optimize the component in any way you want (e.g. using memo with a custom areEqual, using forwardRef, etc.) and to declare exactly the part that is observed (the render phase).

useLocalStore<T, S>(initializer: () => T, source?: S): T (deprecated)

This API is deprecated in 3.*. Use useLocalObservable instead. They do roughly the same, but useLocalObservable accepts an set of annotations as second argument, rather than a source object. Using source is not recommended, see the deprecation message at useAsObservableSource for details

Local observable state can be introduced by using the useLocalStore hook, that runs its initializer function once to create an observable store and keeps it around for a lifetime of a component.

The annotations are similar to the annotations that are passed in to MobX's observable API, and can be used to override the automatic member inference of specific fields.

useAsObservableSource<T>(source: T): T (deprecated)

The useAsObservableSource hook can be used to turn any set of values into an observable object that has a stable reference (the same object is returned every time from the hook).

This API is deprecated in 3.* as it relies on observables to be updated during rendering which is an anti-pattern. Instead, use useEffect to synchronize non-observable values with values. Example:

// Before:
function Measurement({ unit }) {
    const observableProps = useAsObservableSource({ unit })
    const state = useLocalStore(() => ({
        length: 0,
        get lengthWithUnit() {
            // lengthWithUnit can only depend on observables, hence the above conversion with `useAsObservableSource`
            return observableProps.unit === "inch"
                ? `${this.length * 2.54} inch`
                : `${this.length} cm`
        }
    }))

    return <h1>{state.lengthWithUnit}</h1>
}

// After:
function Measurement({ unit }) {
    const state = useLocalObservable(() => ({
        unit, // the initial unit
        length: 0,
        get lengthWithUnit() {
            // lengthWithUnit can only depend on observables, hence the above conversion with `useAsObservableSource`
            return this.unit === "inch" ? `${this.length * 2.54} inch` : `${this.length} cm`
        }
    }))

    useEffect(() => {
        // sync the unit from 'props' into the observable 'state'
        state.unit = unit
    }, [unit])

    return <h1>{state.lengthWithUnit}</h1>
}

Note that, at your own risk, it is also possible to not use useEffect, but do state.unit = unit instead in the rendering. This is closer to the old behavior, but React will warn correctly about this if this would affect the rendering of other components.

Observer batching (deprecated)

Note: configuring observer batching is only needed when using mobx-react-lite 2.0.* or 2.1.*. From 2.2 onward it will be configured automatically based on the availability of react-dom / react-native packages

Check out the elaborate explanation.

In short without observer batching the React doesn't guarantee the order component rendering in some cases. We highly recommend that you configure batching to avoid these random surprises.

Import one of these before any React rendering is happening, typically index.js/ts. For Jest tests you can utilize setupFilesAfterEnv.

React DOM:

import 'mobx-react-lite/batchingForReactDom'

React Native:

import 'mobx-react-lite/batchingForReactNative'

Opt-out

To opt-out from batching in some specific cases, simply import the following to silence the warning.

import 'mobx-react-lite/batchingOptOut'

Custom batched updates

Above imports are for a convenience to utilize standard versions of batching. If you for some reason have customized version of batched updates, you can do the following instead.

import { observerBatching } from "mobx-react-lite"
observerBatching(customBatchedUpdates)

Testing

Running the full test suite now requires node 14+ But the library itself does not have this limitation

In order to avoid memory leaks due to aborted renders from React fiber handling or React StrictMode, on environments that does not support FinalizationRegistry, this library needs to run timers to tidy up the remains of the aborted renders.

This can cause issues with test frameworks such as Jest which require that timers be cleaned up before the tests can exit.

clearTimers()

Call clearTimers() in the afterEach of your tests to ensure that mobx-react-lite cleans up immediately and allows tests to exit.

mobx-react-lite's People

Contributors

alex3683 avatar alsotang avatar barbalex avatar bnaya avatar danielkcz avatar djm avatar fredyc avatar github-actions[bot] avatar grifotv avatar inomdzhon avatar jmxo avatar johnnyreilly avatar johnrichter avatar laysent avatar mcjazzyfunky avatar muhajirdev avatar mweststrate avatar nilshartmann avatar nulladdict avatar olee avatar piotrferens avatar pustovalov avatar renovate-bot avatar renovate[bot] avatar retsam avatar ross-weir avatar roystons avatar samdenty avatar sheerun avatar xaviergonz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mobx-react-lite's Issues

Setup renovate bot

@mweststrate Can you install it and configure for this repo? It's just bunch of DEV dependencies here, but I want to keep them updated.

https://renovatebot.com

I do not have enough permissions to do that. I verified that when I tried to configure the Stale bot that's already enabled for the org.

useComputed might cause some confusion

Yesterday I run into a strange issue when the computed property wasn't being recomputed when contained observables have changed.

function MyComponent() {
  const state = useObservable({ foo: 1 })
  const value = useComputed(() => {
    return state.foo + 10
  })
  return useObserver(() => (
    <div>{value}</div>
  ))
}

https://codesandbox.io/s/k9o2jl0917

One would have thought why the hell it's not working? There is an observable inside observer, it should work. Yea well, the major gotcha is that useComputed is not returning observable, but the resulting computed value itself which is no longer observable.

To make it work it's either about using observer HOC which sometimes might not work either if there are some render prop component being rendered. Another solution is basically the following.

function MyComponent() {
  const state = useObservable({ foo: 1 })
  const value = useMemo(() => mobx.computed(() => {
    return state.foo + 10
  }))
  return useObserver(() => (
    <div>{value.get()}</div>
  ))
}

I am not entirely sure how to approach this. Currently the useComputed does the call to get() so the returned value is not observable anymore. I am thinking it shouldn't probably do it and return a full computed value. Opinions?

Question: Should we extract `PropTypes` out of `mobx-react` ?

I'm trying to use mobx-react-lite instead of mobx-react, and everything just works well except I have to use PropTypes of mobx-react, which force me to depend on the mobx-react package again.

Should we extract PropTypes out of mobx-react ? Maybe a new package named mobx-prop-types or else.

Or should we integrate PropTypes in mobx-react-lite also?

[On hold] React Concurrent mode: avoid side effects in render

Hi! I've been really enjoying mobx-react-lite, thank you so much for this library. I've recently been doing some investigation about using MobX with suspense in react and have noticed some issues with the current useObserver implementation.

First of - components can and will throw. Suspense relies on throwing a promise. I'm a little unsure how MobX handles reactions throwing in general, but right now if you have an observed component and it throws, that upsets MobX and your thrown exception can't be handled in the usual way React does that - with error boundaries and suspense boundaries. That's a relatively simple issue to solve though.

The more serious issue is that once a component throws, React will often completely discard the tree up to the nearest boundary. That means all hooks get rolled back - so you can't rely on each render resulting in a subsequent commit (where useEffect hooks are called and their cleanup functions registered). In general, with the upcoming Concurrent mode, render will often get called more than once.

This can be demonstrated fairly easily with a component like this:

import React from "react";
import ReactDOM from "react-dom";

let isFirstRun = true;
function Suspend() {
  console.log("render");

  if (isFirstRun) {
    isFirstRun = false;
    console.log("suspend");
    throw new Promise(resolve => setTimeout(resolve(1000)));
  }

  React.useEffect(() => {
    console.log("mounted");
    return () => {
      console.log("unmounted");
    };
  }, []);

  return null;
}

function App() {
  return (
    <React.Suspense fallback={<div />}>
      <Suspend />
    </React.Suspense>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

CodeSandbox link

This component logs the following to the console:

render
suspend
render
mounted

So, what does this mean for mobx-react-lite? Right now, useObserver works by creating a reaction in the render phase, then cleaning up when the component unmounts:

const reaction = useRef(
new Reaction(`observer(${baseComponentName})`, () => {
forceUpdate()
})
)
useUnmount(() => {
reaction.current.dispose()
})

But each render call isn't guaranteed to come with a subsequent mount & render. The moment an observed component throws, all the effects of hooks (including the useRef for keeping track of the reaction) get discarded, so we end up leaking reactions all over the place.

I wonder if there's maybe some sort of thing we can do to defer calling .track until a useEffect callback, but the problem there is there can be an async gap between render and commit - when useEffect gets called. What happens when the derived state changes in that time?

Anyway. Thanks again for this project, and for reading all the way through this issue. Super happy to help try and resolve this and submit PRs, but right now it's not clear to me that there's an easy solution. Concurrent mode seems like the future of React though, so I think it's important to figure something out!

useObservable without observer?

Considering that people can just useState or useReducer without any wrapping function, they might be rather confused when switching over to useObservable that they need to wrap component to the observer as well.

@mweststrate Do you think it's actually possible to have useObservable truly reactive without any crutches? I can only think of a forceUpdate based on observe invocation which is essentially what observer is doing, right? However, we also want to avoid double rerender if they do wrap it inside the observer. Feels tricky.

Importing store directly vs importing context

I feel like this needs a little clarification, I can't wrap my head around it to say the least.

With the old Provider/inject gone and us being able to use useContext, whats the difference between exporting/importing the store directly and exporting/importing the context directly?

For example:

// Store.js
class ChatStore {
  messages = [];
}

export default new ChatStore();

// Component.js
import ChatStore from "Store.js";

function MyComponent(props) {
  // can directly access and use ChatStore in here
}

and

// Store.js
class ChatStore {
  messages = [];
}

export default React.createContext(new ChatStore());

// Component.js
import ChatStoreContext from "Store.js";

function MyComponent(props) {
  const store = React.useContext(ChatStoreContext);
  // extra step needed to access store, with seemingly no benefit
}

(examples obviously simplified and missing code necessary to run)

The only difference between these two would be the extra step needed to use the store in the second example.

So, what does using React's Context actually do for us here?

Minified UMD bundle

One leftover from my work. For some reason, the uglify plugin is failing to parse source files properly. I am not sure why is it happening.

$ node build-rollup.js
  20 |     }
  21 |     function configureDOM() {
> 22 |         const { unstable_batchedUpdates } = require("react-dom");
     |        ^ Unexpected token: keyword (const)
  23 |         mobx.configure({ reactionScheduler: unstable_batchedUpdates });
  24 |     }
  25 |     function configureNative() {
{ SyntaxError: Unexpected token: keyword (const)
    at JS_Parse_Error.get (eval at <anonymous> (D:\workspace\github\mobx-react\node_modules\uglify-js\tools\node.js:20:1), <anonymous>:71:23)

yarn build fails

After cloning this repo and running yarn when I run yarn build I get:

Alexanders-MacBook-Pro:mobx-react-lite alexandergabriel$ yarn build
yarn run v1.12.3
$ node build-rollup.js && shx cp src/index.d.ts index.d.ts && shx cp src/index.d.ts native.d.ts && shx cp src/index.d.ts custom.d.ts
{ Error: Could not resolve entry (src/index.js)
    at error (/Users/alexandergabriel/mobx-react-lite/node_modules/rollup/dist/rollup.js:3460:30)
    at /Users/alexandergabriel/mobx-react-lite/node_modules/rollup/dist/rollup.js:21475:17
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:744:11)
    at startup (internal/bootstrap/node.js:285:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:739:3) code: 'UNRESOLVED_ENTRY' }
error Command failed with exit code 255.

Component wont be updated in production build.

My packages:

    "mobx": "^4.6.0",
    "mobx-react-lite": "^0.2.0",
    "normalize.css": "^8.0.1",
    "react": "^16.7.0-alpha.2",
    "react-dom": "^16.7.0-alpha.2",
    "react-pose": "^4.0.1",
    "react-router-dom": "^4.3.1",
    "react-scripts": "^2.1.1",
    "react-typing-animation": "^1.4.0",
    "rebass": "^3.0.0-11",
    "styled-components": "^4.1.1",
    "styled-system": "^3.1.11",
    "styled-tools": "^1.6.0",
    "typescript": "^3.1.6"

What's the problem:

  1. I have an AppStore.tsx:
export default class AppStore {
  @observable
  steps: ReactElementArray = [];

  @action.bound
  nextStep() {}
}

The logic is, there will be a component array. After each step finishes, it will call nextStep(), then a new step(a component) will be pushed into this.steps.

  1. And it will trigger render of this <ChatScreen>:
import React, { useContext } from 'react';
import { Flex } from 'rebass';
import { RouteComponentProps } from 'react-router-dom';
import { observer } from 'mobx-react-lite';
import { PoseGroup } from 'react-pose';
import Header from '../../components/Header/Header';
import { Box } from '../../components/buildingBlocks';
import { AppStoreContext } from '../../store/store';

const ChatScreen: React.FC<
  RouteComponentProps
> = () => {
  const { steps } = useContext(AppStoreContext);

  return (
    <Flex width={1} flexDirection="column">
      <Header title="i am empty title" />
      <Box>
        <PoseGroup animateOnMount>{steps}</PoseGroup>
      </Box>
    </Flex>
  );
};

export default observer(ChatScreen);
  1. Everything works fine in the development build but not the production build. The production build will execute step 1, but after a new step been added to this.step(I checked, the length increased). It won't trigger re-render of ChatScreen.

  2. Basically, the component of second step won't be rendered at all.

const LocationDropDown: React.FC<IBaseStepType> = ({
  callback,
}) => {
  console.log('i am LocationDropDown');
  return (
    <FlexCard boxShadow={2}>
      <ProvincesDropDown />
      <CitiesDropDown callback={callback} />
    </FlexCard>
  );
};

export default observer(LocationDropDown);

Because the console.log won't be shown in the console.

Any idea why?

It's a private repo. Invitation can be sent.

I can't go back to the no-hook and inject age for this green field project, no.........

Could you please provide more code example for a migration

Hi there,

I had MobX implemented in my current project and now experiment the new hook API. I have a single store implemented but don't know how to start a multi-store implementation since the Provider component got cut out.

Are you suggesting to use context to maintain observable object list and use the useContext to consume it?

Cheers

[With React-router] Warning: Failed prop type: Invalid prop `component` of type `object` supplied to `Route`, expected `function`.

Thanks for the quick work!
Super excited to try it out!
A quick issue, don't know if I miss something.

  "dependencies": {
    "mobx": "4",
    "mobx-react-lite": "^0.2.0",
    "react": "^16.7.0-alpha1",
    "react-dom": "^16.7.0-alpha1",
    "react-router-dom": "^4.3.1",
    "react-scripts": "2.1.1"
  },

How to reproduce:

  1. create-react-app try
  2. edit the App.js to the following:
import React from "react";
import { observer } from "mobx-react-lite";
import { BrowserRouter, Route } from "react-router-dom";

const Test = observer(() => <div>Quick Test</div>);

function App() {
  return (
    <BrowserRouter>
      <Route path="*" component={Test} />
    </BrowserRouter>
  );
}

export default App;
  1. yarn start, rendering works fine!

  2. check the console:

index.js:1452 Warning: Failed prop type: Invalid prop component of type object supplied to Route, expected function.
in Route (at App.js:10)
in App (at src/index.js:7)

HOC inject example

Hi all.

I've been trying to find a way to create an inject helper function like in the README:

function inject(useSelector, baseComponent) {
    const store = useContext(StoreContext)
    const selected = useSelector(store)
    // optional memo essentially making a pure component
    return  baseComponent({ ...selected })
}

React throws a Hooks can only be called inside the body of a function component. error. I'm a bit of a noobie but haven't been able to find a way around this. It seems we may have to just use useContext and then a selector in the components themselves.

Can I please get your thoughts on this? Is my conclusion correct? Thanks :)

Idea: Add useAction hook?

Looking at the example in README, I have a feeling that this is going to be rather a repeated pattern.

    const todos = useObservable(new Map<string, boolean>())
    const addTodo = React.useCallback(() => {
        todos.set(todoRef.current.value, false)
        todoRef.current.value = ""
    }, [])
    const toggleTodo = React.useCallback((todo: string) => {
        todos.set(todo, !todos.get(todo))
    }, [])

So I am thinking something along the lines of another tiny wrapper around the useCallback with empty inputs array by default. Possibly it can use runInAction internally. Thoughts?

@xaviergonz @mweststrate

Kickoff

Ok, we have a clean history, version goes from 0.0.1 for now, we can figure that out later. It's based on the copy from @johot's fork and I've removed some obvious stuff like Provider/inject and some tests that were working with these two, It's still heavily broken, but good enough for the kickoff.

I certainly need help as I am not using mobx-react in its full glory, eg. prop types, devtools, SSR, transactions... so a lot of stuff does not make a full sense to me just yet.

@mweststrate Would you be against writing it directly in TypeScript? I assume it's ok considering that mobx itself is full blown TypeScript project. Would be certainly much easier to maintain typings.

Let's keep this issue to track who is going to do what do avoid duplicate work.

I am going to focus purely on the observer/Observer pair, rewrite to TypeScript and hopefully make some tests.

Shouldn't it be useObservable<T>(init: () => T)): T ?

Maybe that sounds a bit like a useState(initialValue) vs. useState(() => initialValue) discussion, but shouldn't it be useObservable<T>(init: () => T): T instead of useObservable<T>(initialValue: T): T?
That would also help to handle unobserved data without useRefs, wouldn't it?

Can't work with old webpack uglify plugin?

ERROR in Encountered an error while minifying js/vendor.891a096d.js:
SyntaxError: Unexpected token: keyword (const)
  27927 |
  27928 | function useObservable(initialValue) {
> 27929 |     const observableRef = Object(__WEBPACK_IMPORTED_MODULE_1_react__["useRef"])(null);
        |    ^
  27930 |     if (!observableRef.current) {
  27931 |         observableRef.current = Object(__WEBPACK_IMPORTED_MODULE_0_mobx__["observable"])(initialValue);
  27932 |     }

The build version of this package has es6 syntax,but i use webpack-parallel-uglify-plugin,so it breaks my app. Do I have to upgrade my uglify plugin or config my babel-loader to transpile it first?

Include runnable example in source code?

I've managed to successfully migrate a mobx v5 and mobx-react project over to mobx-react-lite, and was pleasantly surprisedly how relatively easy it was.

That being said, I can see the process being daunting for people that aren't as familiar with functional composition, use of context, etc.

@FredyC is there any desire to have a runnable example project? If so, I will happily volunteer to create something that can serve as way to complement the existing documentation in the readme.

NamedExoticComponent cannot be used for observer

): React.NamedExoticComponent<P>

Just encountered this. Apparently ExoticComponent cannot be used to represent an actual callable component. It's useful for memo, lazy and similar.

export const ContrivedComponent = observer(({ children }) => {
  return React.Children.only(children)
})

Since the ExoticComponent is lacking children attribute, such component cannot be used anywhere where children are needed. It's possible to do observer(({ children }: { children: ReactNode }) but that doesn't feel good at all.

Ideas @xaviergonz?

useDisposable not running

I'm seeing some sort of bug on this in React 16.8.0-alpha.1, where it runs the first time and then doesn't run again on any update. Opening here until I get a chance to check into it further.

displayName is set after observer, observer could use a proxy to fetch it

Since we write it as:

const Item = observer(() => <div />)
Item.displayName = 'Item'

It won't pick up the displayName. Perhaps observer could use a proxy to pick that up:

function observer(a) {
  return new Proxy(a, { set() { /* pick up displayName and apply it to reaction here */ } })
}

Not sure if Mobx accepts it "after its created" so to speak, but for now I just see a ton of observer() in my reaction logs which isn't very helpful.

Anyone got it working with react-hot-loader yet?

First off really great work on this one @FredyC so much progress!

Was just wondering if anyone have tried using this with hot reloading? I'm testing it now and it seems that as soon as I wrap a functional component in the observer used in mobx-react-lite hot reloading stops working. I am using this sample currently as a base: https://github.com/gaearon/react-hot-loader/tree/master/examples/typescript-no-babel (but upgrades all deps to latest version).

The regular mobx-react seems to work though...

Add example or test case for mixing with vanilla useState

It's often valuable during refactoring of code to be able to shift from one state management approach to another. It would be great to be able to have both useState and mobx in the same component to allow gradual refactoring in one direction or the other.

New Context - Performance

I’m trying to figure out how to handle global state in a new application that is using mobx, mobx-react-lite, and react hooks. The basic plan (what I would like to do) is to have the global state available via context and then use hooks in components to select/filter data and initiate mutations. The question/issue revolves around using react’s new context and possible performance implications. I’ve been following discussions on the react-redux project and elsewhere and there seems to be a concern with how context currently works. The issues are best described by Mark Erikson here.

Here is the relevant portion:

Context API Limitations

At first glance, React's new createContext API appears to be perfectly suited for the needs of a library like React-Redux. It's built into React, it was described as "production-ready" when it was released, it's designed for making values available to deeply-nested components in the tree, and React handles ordering the updates in a top-down sequence. The <Context.Provider> usage even looks very similar to React-Redux's <Provider>.

Unfortunately, further usage has shown that context is not as well suited for our use case as we first thought. To specifically quote Sebastian Markbage:

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

React-Redux and Hooks

In addition to concerns about performance and state update behaviors, the initial release of the React Hooks API will not include a way to bail out of updates triggered by a context value update. This effectively blocks React-Redux from being able to ship some form of a useRedux() hook based on our current v6 implementation.

To again quote Sebastian:

I realize that you've been put in a particularly bad spot because of discussions with our team about concurrent mode. I apologize for that. We've leaned on the use of context for propagation which lead you down the route of react-redux v6. I think it's generally the right direction but it might be a bit premature. Most existing solutions won't have trouble migrating to hooks since they'll just keep relying on subscriptions and won't be concurrent mode compatible anyway. Since you're on the bleeding edge, you're stuck in a bad combination of constraints for this first release. Hooks might be missing the bailout of context that you had in classes and you've already switched off subscriptions. :(

I'll pull out one specific sentence there for emphasis:

I think [v6's use of context is] generally the right direction but it might be a bit premature.

Does mobx-react-lite workaround this somehow or is this somehow not relevant to mobx?

Bad type, Generic requires one type argument

In 1.0.0:

node_modules/mobx-react-lite/dist/ObserverComponent.d.ts:6:75 - error TS2314: Generic type 'ReactElement<P>' requires 1 type argument(s).

6 declare function ObserverComponent({ children, render }: IObserverProps): ReactElement<any, string | ((props: any) => ReactElement<any, string | any | (new (props: any) => import("react").Component<any, any, any>)> | null) | (new (props: any) => import("react").Component<any, any, any>)> | null;

Replace "experimental" in GitHub repo description with e.g. "16.8+" or "modern"

Now that Hooks are an officially released feature in 16.8 (and this repo is 1.0.0, ready for production use), the word "experimental" could be removed from the GitHub repo description text (and elsewhere it may be used). Not sure what is a better replacement term.

As well, one thing not clear in the README is whether we can still use class-based components elsewhere in the hierarchy, not using mobx but possibly their children might? Otherwise I suppose mobx-react is still recommended?

Question: suggested best practice for holding onto a store constructed with props

First up: we've switched a project over from mobx-react to hooks and mobx-react-lite and are loving it. Thanks.

Second: apologies for a question as a Github issue.

A common pattern we have is a component wanting to hold onto a mobx store only for the component's lifetime, but the store constructor is passed a prop and we want to re-create the store if the prop changes.

So far I'm using useMemo to manage this. For example, with a react-router param passed in:

const DashboardPage: FC<RouteComponentProps<IRouteParams>> = ({ match }) => {
  const { siteId } = match.params
  const store = useMemo(() => new DashboardStore(siteId), [siteId])

While this works fine the React reference for useMemo has a warning:

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to β€œforget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo β€” and then add it to optimize performance.

The store being recreated would be bad, the user losing data. I'm assuming this is a common use case not just for me. What is the 'mobx recommended' way to handle this?

Idea: useObservableRef

Basically the same thing mobx-react does right now with props to make them "observable". It just keeps track of the reference of the object, since anytime the props change then its ref changes, but in a more generic way. Basically the poor mans version of useObservableProps :)

const obsProps = useObservableRef(props);

// since any time the props change the reference changes this is ok
useComputed(() => `${obsProps.firstName} ${obsProps.lastName}`);

function useObservableRef(data: any) {
  const ref = useRef(observable.box(data));
  const update = useCallback(runInAction(() => {
    ref.current.set(data);
  }));

  update();
  return ref.current.get();
}

Good things:

  • Makes using computed over props easier (no need to inject deps)
  • Makes porting current mobx-react codebases easier

Possible issues:

It is compatible with useObserver/Observer, but when using observer it might have double rendering issues since the ref observable would change inside the reaction tracking part.
Possible solutions:

  • one reaction per render solves it, but performance might be hurt.
  • drop observer?
  • warn in the docs strongly about this?
  • add an option in observer that does this for you outside the useObserver (at least for props) and send those observable ref props as props object to the base component? (this is actually what mobx-react mostly does actually)

Possible memory leak in useObservableEffect

Probably not, but I just noticed in test coverage that the disposer is never called.

    useEffect(
        () => () => {
            if (disposerRef.current) {
                disposerRef.current()
            }
        },
        inputs
    )

I tried calling unmount, modify the store value again and render counters were the same so, in the essence, it seems to be working. However, the coverage hasn't changed with such test so it never gets called either way it seems.

In theory, it could be caused by the asynchronous nature of useEffect, but I tried switching it to useLayoutEffect which should be synchronous, but nothing has changed.

I am not really sure what might be wrong here and how to test it properly.

@xaviergonz

'lite' depends on full mobx?

Hi, I might be misunderstanding the full intention here--this package has the moniker "lite" and it depends on the full mobx package. By "lite" does it mean a smaller subset adapted for React Hooks and at a much smaller bundle size or just the former without the (necessarily) latter?

[Discuss] useObserver, utility hooks

I'm doing some experimentation/refactoring in light of #38 and #94, and just in general catching up. I have a few places where useComputed works quite well. What I'm starting to think is the pattern of useObserver as the last return is actually the root of a few problems.

For one, it means you can never do an early return (since a hook is the last return), which is a huge annoyance to me.

Second, using the useObserver hook as the last return creates a split in the component of unobserved/observed behavior, which I think is just adding complexity.

I've started going back to wrapping components with observer directly and liking how it feels so far. Under the hood it's still using useObserver, but it's around the entire component.

Happy to be completely wrong on this or have my mind changed, but is it possible useObserver is really something that should be more of an internal implementation detail and not exposed to the user?

Question: hoist-non-react-statics

First thanks for this library, I love the updates compared to the old package. When implementing this in a Next.js codebase, I ran into issues where the observer HOC doesn't hoist non-react-statics e.g. getInitialProps. Is this something we should add? If so, I would be happy to make a PR.

If not, the workaround is easy enough

const ObservedComponent = observer(Component);
ObservedComponent.getInitialProps = () => { };

Get rid of (almost) all utility hooks

Update

After a lengthy discussion, we have agreed on the removal of useComputed and useDisposable. The useObservable hook will be renamed to useAsObservableSource and meant primarily for turning props/state data into observable. These can be then safely used in a new hook useLocalStore which will support lazy-init and serve as the main way of constructing observables within a component.

Check out a comment with an initial proposal: #94 (comment)


Time has come to start considering this. There have been some discussions lately that got me convinced these utilities should not have been in a package in first place. I think we got carried away in here, most likely because it felt good to have some custom hook 😎 Ultimately, people might get the wrong idea that to use MobX in React they need to use these specific hooks.

I think it's better to do this sooner than later before people start using these utilities too much and would need to painfully migrate later.

As the first step, I would like to focus on preparing a website dedicated to MobX in React that would show several recipes and DIY solutions. I would like to hear some recommendations on what toolkit would be the best candidate for such a site. Gatsby?

The idea of some separate package with utility hooks is not eliminated, but it should go hand to hand with new docs to thoroughly explain the concepts and not to just blindly use the hook without fully understanding implications.

useComputed

The most controversial and not even working properly. After the initial pitfalls, I haven't used this anywhere. Is someone using it successfully?

useDisposable

As @mweststrate discovered today, there is not much of the benefit to this as the React.useEffect does exactly the same. The only difference is access to early disposal function. Personally, I haven't used for anything just yet. I would love to hear use cases if there are any. I am almost ashamed I haven't realized this before and just blindly used it 😊

useObservable

Probably the most useful out of these three and the most discussed (#72, #7, #22, #69) also. It's clear that it's not only confusing but in its current form, it's wrong altogether. Personally, I have rather used React.useState for a component local state which is so easy and doesn't require any observer. There is not much performance gain anyway unless <Observer /> is used. For the shareable state, it seems better to just use Context and build such a state in a way people like. Also, it makes a little sense to be resetting the whole MobX state based on props change.

useComputed outside useObserver is uncached

since useComputed returns a plain value, this means that if it is used outside a useObserver block it will never cache, and therefore just be a more expensive "always calculate this"

e.g.

const sum = useComputed(() => 1+2); // this will return the plain value 3, and since get() is called outside observation it is not cache
useObserver(() => {
  // if we use sum here it will just use the plain value 3, which cannot be tracked
})

mobx-react-lite 0.3.7 does not work with react 16.8.0

I tested out an upgrade on react app I'm building from 16.8.0-alpha.1 to 16.8.0. Best I can tell on some initial analysis, it looks like function components wrapped in observer() aren't rendering when observables are modified in an action.

I verified that this issue is present all the way back to 0.3.4, so it's highly likely that something in the linked changelog below has resulted in a breaking change with mobx-react-lite.

https://reactjs.org/blog/2019/02/06/react-v16.8.0.html#hooks-changelog-since-alpha-versions

I went ahead and created a branch on my mobx-react-lite fork to test the 16.8.0 upgrade, and the result is most of the tests run by jest fail as well.

I'll spend some time digging into what the breaking change is and post any updates to this issue.

Rename useObservableEffect to useDisposable

@xaviergonz I just realized that useObservableEffect might not be needed. The only benefit I see is that you get the disposer to the outer scope, but I am not sure about a single use case scenario where that could be useful (yet).

This will work exactly the same if I am not mistaken since reaction/autorun/when are all returning a function that will be called on unmount anyway. Having a memoized ref to that disposer is such a tiny bonus.

   useEffect(() =>
       reaction(
           () => [props.firstName, props.lastName],
           () => {
               // send this to some server
           }
       )
   , [])

It's not like it's completely useless, but feels like it might confuse users that there is some other benefit to it. Correct me, if I am wrong, please.

Idea: Add useObserver

function MyObserverComponent(props) {
  const observe = useObserver();

  return observe(() => {
    return <div>props.observable.x</div>
  });
}

I think it could have several advantages:

  1. you can specify the exact specific piece of code that runs inside the reaction tracking, so you can do observable mutations outside it easily without triggering unwanted renders
  2. you can memo your component, forward ref it or whatever you want without observer getting in the middle

Maybe the only disadvantage though is that you actually have to write "observe" or whatever name you assigned over the render phase though

Maybe offer it as an alternative to "observer" at least for advance use cases?

observer uses something like that internally anyway

Originally posted by @xaviergonz in #7 (comment)

How to use "observer" inside a custom hook?

I have made a custom hook that looks something like that:

export function useProvider(loadingStatusStore) {
  const [providedData, setProvidedData] = useState(undefined);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState(false);
  const [sources, setSources] = useState([]);

  useEffect(
    () => {
      (async () => {
        try {
            const providedData = await fetch(metadata);

            setProvidedData(providedData);
            setIsLoading(false);
        } catch (e) {
          setIsError(true);
        }
      })();
    },
    loadingStatusStore ? [loadingStatusStore.lastRefreshDate] : []
  );

  return [providedData, { sources, isLoading, isError }];
}

I thought to use an observable property from a global (single-instance) store loadingStatusStore.lastRefreshDate... But how do I use one inside a custom hook?

observer expect a component and useObserver complaints that it must be run inside a functional component (which is weird because this hook does run inside a functional component).

The only way I found to make it work is to make the component that uses this hook an observer... Which sort=of makes sense but it is a bit too magical - The user of this custom hook needs somehow to know to make the using component an observer without a clear indication.

What is the correct way to handle that?

If there is no other way, I'd be happy at least to check if I am running inside an observer component and log an error if I don't. But can I know that?

Q: What benefits does useObservable have over using setState/Effect directly?

I'm a big fan of Mobx, and interested in hooks, but struggling to see the benefit of mobx-react-lite.
An external MobX store with @observable and @computeds still seem very valuable, but useObservable and useComputed hooks don't seem very useful.

useObservable:

function ObservePerson(props) {
    const person = useObservable({ name: "John" })
    return (
        <div>
            {person.name}
            <Observer>{() => <div>{person.name}</div>}</Observer>
            <button onClick={() => (person.name = "Mike")}>No! I am Mike</button>
        </div>
    )
}

useState:

function Person(props) {
    const [name, setName] = useState("John");
    return (
        <div>
            <div>{name}</div>
            <button onClick={() => setName("Mike")}>No! I am Mike</button>
        </div>
    )
}

They just seem 'different'. Is there a benefit I'm missing to help explain when people ask?

Finish or discard useComputed ?

Continuing from my monolog . The implementation (and test) are in the repo, but not quite working. Would love to hear some opinions if it's needed and what's wrong with it or ditch it completely.

useInject hook

What do you think of a useInject hook?

Something like this (copied from the inject example of mobx-react):

const user = observable({
  name: "Noa"
});

const NameDisplayer = () => {
  const { name } = useInject(stores => ({
    name: stores.userStore.name
  }));
  return <h1>{name}</h1>;
};

const App = () => (
  <Provider userStore={user}>
    <NameDisplayer />
  </Provider>
);

ReactDOM.render(<App />, document.body);

We really like the inject(mapper) pattern and it's our default way to pass state to our components. It also avoids the need to wrap components with observer.

It could use the next context API, and it could also include a Provider so people don't have to import the store/context time after time. And actually, I'd love it to be renamed to useStores. Much clearer:

const NameDisplayer = () => {
  const { name } = useStores(stores => ({
    name: stores.userStore.name
  }));
  return <h1>{name}</h1>;
};

Any thoughts?

Getting a "Can't resolve 'react-native'" error message

Great work getting this up an running @FredyC! However I am currently getting the following error message when creating a new project using create-react-app 2.1 (with --typescript not sure if it works without TypeScript):

Failed to compile.

./node_modules/mobx-react-lite/dist/index.module.js
Module not found: Can't resolve 'react-native' in 'C:\repos\testproj\node_modules\mobx-react-lite\dist'

Not sure what the correct fix for this is. I can't see the same type of solution in the old mobx-react. If I remove this from the dist it starts working again:

function configureNative() {
    const { unstable_batchedUpdates } = require("react-native");
    configure({ reactionScheduler: unstable_batchedUpdates });
}

Using existing observables / global state

I was wondering how one is supposed to use existing observable objects with this library.

From what I understand the useObservable creates a new observable that is only available to the component itself.

I have tried the following code for tracking global state successfully

import { autorun, observable } from "mobx"
import { useState, useEffect } from "react"

const useGlobalState = (getValue: () => any) => {
  const value = useState(getValue())
  useEffect(() => {
    const destructor = autorun(() => {
      value[1](getValue())
    })
    return () => {
      destructor()
    }
  }, [])
  return value[0]
}

export default useGlobalState

Using this hook would offer automatic subscriptions like this

import { action, observable } from "mobx"
import * as React from "react"
import useGlobalState from "./useGlobalState"

const state = observable({
  other: "another text"
})

const ExampleComponent: React.FunctionComponent<any> = (props) => {
  const stateValue = useGlobalState(() => state.other)
  return (
    <div>
      Current value is: {stateValue} <br />
      <input value={stateValue} onChange={(event: any) => {
        action(() => {
          state.other = event.nativeEvent.target.value
        })()
       }} />
    </div>
  )
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.