Coder Social home page Coder Social logo

mobx-react-lite's Introduction

mobx-react-lite

Build StatusCoverage Status

Join the chat at https://gitter.im/mobxjs/mobx

This is a next iteration of mobx-react coming from introducing React hooks which simplifies a lot of internal workings of this package.

You need React version 16.8.0 and above

Class based components are not supported except using <Observer> directly in its render method. If you want to transition existing projects from classes to hooks (as most of us do), you can use this package alongside the mobx-react just fine. The only conflict point is about the observer HOC. Subscribe to this issue for a proper migration guide.

NPM

Project is written in TypeScript and provides type safety out of the box. No Flow Type support is planned at this moment, but feel free to contribute.

API documentation

<Observer/>

Observer is a React component, which applies observer to an anonymous region in your component. It takes as children a single, argumentless function which should return exactly one React component. The rendering in the function will be tracked and automatically re-rendered when needed. This can come in handy when needing to pass render function to external components (for example the React Native listview), or if you want to observe only relevant parts of the output for a performance reasons.

import { Observer, useObservable } from "mobx-react-lite"

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>
    )
}

Edit ObservePerson

In case you are a fan of render props, you can use that instead of children. Be advised, that you cannot use both approaches at once, children have a precedence. Example

import { Observer, useObservable } from "mobx-react-lite"

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

observer<P>(baseComponent: FunctionComponent<P>, options?: IObserverOptions): FunctionComponent<P>

Function that converts a function component into a reactive component, which tracks which observables are used automatically re-renders the component when one of these values changes. Observables can be passed through props, accessed from context or created locally with useObservable.

As for options, it is an optional object with the following optional properties:

  • forwardRef: pass true to use forwardRef over the inner component, pass false (the default) otherwise.
import { observer, useObservable } from "mobx-react-lite"

const FriendlyComponent = observer(() => {
    const friendNameRef = React.useRef()
    const data = useObservable({
        friends: [] as string[],
        addFriend(favorite: boolean = false) {
            if (favorite === true) {
                data.friends.unshift(friendNameRef.current.value + " * ")
            } else {
                data.friends.push(friendNameRef.current.value)
            }
            friendNameRef.current.value = ""
        },
        get friendsCount() {
            return data.friends.length
        }
    })

    return (
        <div>
            <b>Count of friends: {data.friendsCount} </b>
            {data.friends.map(friend => (
                <div>{friend}</div>
            ))}
            <hr />
            <input ref={friendNameRef} />
            <button onClick={data.addFriend}>Add friend </button>
            <button onClick={() => data.addFriend(true)}>Add favorite friend</button>
        </div>
    )
})

Edit FriendlyComponent

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

Low level implementation used internally by observer. 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). One good thing about this is that if any hook changes an observable for some reason then the component won't rerender twice unnecessarily.

The following optional parameters are available:

  • baseComponentName: a string that will be used as part of the reaction name.

As for the options, the following are available:

  • useForceUpdate: optional custom hook that should make a component re-render (or not) when changes are detected.
import { memo } from "react"
import { useObserver, useObservable } from "mobx-react-lite"

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

Notice of deprecation

We are considering deprecation and removal of following utilities from the package. Come join the discussion: mobxjs#94

useObservable<T>(initialValue: T): T

React hook that allows creating observable object within a component body and keeps track of it over renders. Gets all the benefits from observable objects including computed properties and methods. You can also use arrays, Map and Set.

Warning: With current implementation you also need to wrap your component to observer. It's also possible to have useObserver only in case you are not expecting rerender of the whole component.

import { observer, useObservable, useObserver } from "mobx-react-lite"

const TodoList = () => {
    const todos = useObservable(new Map<string, boolean>())
    const todoRef = React.useRef()
    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))
    }, [])

    return useObserver(() => (
        <div>
            {Array.from(todos).map(([todo, done]) => (
                <div onClick={() => toggleTodo(todo)} key={todo}>
                    {todo}
                    {done ? " ✔" : " ⏲"}
                </div>
            ))}
            <input ref={todoRef} />
            <button onClick={addTodo}>Add todo</button>
        </div>
    ))
}

Lazy initialization

Lazy initialization (similar to React.useState) is not available. In most cases your observable state should be a plain object which is cheap to create. With useObserver the component won't even rerender and state won't be recreated. In case you really want a more complex state or you need to use observer, it's very simple to use MobX directly.

import { observer } from "mobx-react-lite"
import { observable } from "mobx"
import { useState } from "react"

const WithComplexState = observer(() => {
    const [complexState] = useState(() => observable(new HeavyState()))
    if (complexState.loading) {
        return <Loading />
    }
    return <div>{complexState.heavyName}</div>
})

Edit TodoList

Note that if you want to track a single scalar value (string, number, boolean), you would need a boxed value which is not recognized by useObservable. However, we recommend to just useState instead which gives you almost same result (with slightly different API).

useComputed(func: () => T, inputs: ReadonlyArray<any> = []): T

Another React hook that simplifies computational logic. It's just a tiny wrapper around MobX computed function that runs computation whenever observable values change. In conjuction with observer the component will rerender based on such a change.

const Calculator = observer(({ hasExploded }: { hasExploded: boolean }) => {
    const inputRef = React.useRef()
    const inputs = useObservable([1, 3, 5])
    const result = useComputed(
        () => (hasExploded ? "💣" : inputs.reduce(multiply, 1) * Number(!hasExploded)),
        [hasExploded]
    )

    return (
        <div>
            <input ref={inputRef} />
            <button onClick={() => inputs.push(parseInt(inputRef.current.value) | 1)}>
                Multiply
            </button>
            <div>
                {inputs.join(" * ")} = {result}
            </div>
        </div>
    )
})

Notice that since the computation depends on non-observable value, it has to be passed as a second argument to useComputed. There is React useMemo behind the scenes and all rules applies here as well except you don't need to specify dependency on observable values.

Edit Calculator

useDisposable<D extends TDisposable>(disposerGenerator: () => D, inputs: ReadonlyArray<any> = []): D

The disposable is any kind of function that returns another function to be called on a component unmount to clean up used resources. Use MobX related functions like reaction, autorun, when, observe, or anything else that returns a disposer. Returns the generated disposer for early disposal.

Example (TypeScript):

import { reaction } from "mobx"
import { observer, useComputed, useDisposable } from "mobx-react-lite"

const Name = observer((props: { firstName: string; lastName: string }) => {
    const fullName = useComputed(() => `${props.firstName} ${props.lastName}`, [
        props.firstName,
        props.lastName
    ])

    // when the name changes then send this info to the server
    useDisposable(() =>
        reaction(
            () => fullName,
            () => {
                // send this to some server
            }
        )
    )

    // render phase
    return `Your full name is ${props.firstName} ${props.lastName}`
})

Macro Version

Think of the macro version of useObserver like you would think of observer from mobx-react, just with different syntax.

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

// useObserver comes from the macro
import { useObserver } from "mobx-react-lite/macro"

// other hooks come as normal from the main library
import { useObservable } from "mobx-react-lite"

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

The macro rewrites your code at build/compile-time into the following:

import { useObserver } from "mobx-react-lite"
import { useObservable } from "mobx-react-lite"

// NOTE: MACRO GENERATED TRANSFORMATION:
const Person = props => {
    return useObserver(function useObserverRenderHook() {
        const person = useObservable({ name: "John" })
        return (
            <div>
                {person.name}
                <button onClick={() => (person.name = "Mike")}>No! I am Mike</button>
            </div>
        )
    })
}

The macro solves a few problems:

  • More hook-like code structure, less verbosity.
  • If you use observer(props => ...break a hook rule) you will not be warned - you may think you've done everything correctly until you eventually realize the linter is not analyzing these for you.
  • If you use useObserver(() => ...useX()) directly, you are breaking a rule of hooks as per the current linting rules (calling a hook inside of a callback). You can get around this and please the linter by naming your inner function such as the macro output demonstrates above, but that leads to additional code vebosity.
  • By sticking to this simple pattern for components that handle observable data you can avoid many pitfalls associated with using Observer or useObserver directly as the last return but not sole statement. The pitfalls are related to expecting reactions in scopes that are not under observation. The macro enforces expected behavior.

Server Side Rendering with useStaticRendering

When using server side rendering, the components are rendered only once. Since components are never unmounted, observer components would in this case leak memory when being rendered server side. To avoid leaking memory, call useStaticRendering(true) when using server side rendering which essentially disables observer.

import { useStaticRendering } from "mobx-react-lite"

useStaticRendering(true)

This makes sure the component won't try to react to any future data changes.

Why no Provider/inject?

Historically the Provider was useful because a lot of boilerplate was required due to experimental (but widely used) context. By introducing new Context API in React 16.3 it's fairly easy to do this.

const StoreContext = React.createContext(createStore())

// a file with a component
function ConnectedComponent() {
    // replacement for inject
    const store = useContext(StoreContext)
}

If you need to create a store sometimes later, you can just render StoreContext.Provider somewhere in tree.

const StoreContext = React.createContext()

function App({ children }) {
    return <StoreContext.Provider value={createStore()}>{children}</StoreContext.Provider>
}

What about smart/dumb components?

The React hooks don't force anyone to suddenly have a state inside a dumb component that is supposed to only render stuff. You can separate your concerns in a similar fashion.

import { createSelector } from "react-selector-hooks"

const userSelector = createSelector(({ user }) => ({
    name: user.name,
    age: user.age
}))

function UiComponent({ name, age }) {
    return (
        <div>
            <div>Name: {name}</div>
            <div>Age: {age}</div>
        </div>
    )
}

export default () => {
    // you may extract these two lines into a custom hook
    const store = useContext(StoreContext)
    const data = userSelector(store)
    return UiComponent({ ...data })
    // perhaps wrap it inside observer in here?
    return observer(UiComponent({ ...data }))
}

It may look a bit more verbose than a classic inject, but there is nothing stopping you to make your own inject HOC which is so much easier since everything is just a function.

// make universal HOC

const inject = (useSelector, baseComponent) =>
    React.useMemo(props => {
        const store = useContext(StoreContext)
        const selected = useSelector(store)

        return baseComponent({ ...selected, ...props })
    })

// use the HOC with a selector

export default inject(userSelector, UiComponent)

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.