Use mobx in react to take over all the state

Original link: https://blog.rxliuli.com/p/1fcd1517c14c4e68b764d7e6df071805/

foreword

In react hooks, useEffect is the most commonly used hooks function, but its api experience of manually managing dependency status has been criticized for a long time. There are countless articles on how to use useEffect correctly in the community, but it still can’t stop more newcomers. The fact of using this api correctly is also dubbed the react newcomer wall. Moreover, among popular web frameworks, only react needs to manually manage hooks dependencies. Other frameworks such as vue, svelte and solidjs do not require manual dependency management. The recent hot discussion about signals in the react community also reflects that more people have realized the badness of this kind of dx, and preact even officially supports signals .

Why did signals become popular all of a sudden?

Well, my guess is the adoption of solidjs. Although vue and svelte do not need to manually manage dependencies, they are very different from react in the way they are written. They all have their own template syntax. Vue even requires additional plug-ins to use jsx, and the experience is not very good, so it is better Many see them as differences between different frameworks – not which hooks api is better. While solidjs fully adopts the jsx syntax and community-related tool chain, but the state management is more friendly to developers. Using useEffect/useMemo/useCallabck no longer needs to manually manage dependencies, but automatically handles them in an efficient manner.
The following video best expresses my views

The one-pass operation of react is as fierce as a tiger, and as a result, a signal solves all problems more elegantly.
The operations shown are

  • virtual dom
  • immutable data
  • hooks
  • dependent array
  • Compiler optimizations and automatic caching

for example

Dependency transmission has dependencies, and the functions useEffect/useMemo/useCallback all depend on deps array parameters. And they can also depend on each other. For example, the value of useMemo can be used as a dependency by useCallback. In short, if you use these common react hooks, you must manually manage the dependency graph between them. If not managed correctly, very subtle bugs can arise. React provides eslint rules to check, but on the one hand, not all projects use eslint. On the other hand, this eslint rule is usually too strict and must be manually closed in some cases. For example, when using useEffect, you want to trigger according to the change of a value Side effect, but at the same time needs to read the latest b value, and eslint rules blow up in this respect. On the other hand, the status of react cannot be read immediately after modification. This is not caused by react hooks, but a problem that has always existed in react.

Status update and read

The traditional mental model, you read the latest value immediately after modifying the variable.

 1
2
3
4
 let i = 0
console . log (i) // 0
i += 1
console . log (i) // 1

The mental model of react uses await new Promise(resolve => setTimeout(0, resolve)) to wait for the next cycle to read the latest value.

 1
2
3
4
5
6
 const [i, setI] = useState ( 0 )
console . log (i) // 0
setI (i + 1 )
console . log (i) // 0
await new Promise ( ( resolve ) => setTimeout ( 0 , resolve))
console . log (i) // 1

The main problem with this approach is that it is verbose, not intuitive and not particularly reliable.


Or use a temporary variable to hold the new value and use the new value later.

 1
2
3
4
5
 const [i, setI] = useState ( 0 )
console . log (i) // 0
const newI = i + 1
setI (newI)
console . log (newI) // 1

This method may be used more in practice, mainly because additional variables need to be created


Or use immer, you can use produce to wrap a layer, so that the latest value can be read after modification in the callback.

 1
2
3
4
5
6
7
8
9
10
11
12
 import produce from 'immer'

const [i, setI] = useState ( 0 )
console . log (i) // 0
setI (
produce (i, ( draft ) => {
draft += 1
console . log (draft) // 1
return draft
}),
)
console . log (i) // 0

But this function does not work well with asynchronous functions. For example, the following code is impossible, because when the callback accepted by produce returns a Promise, the result of the produce function will also be a Promise, which is not available for the set function of react. Of course you can add await, but you need to merge and split when there are multiple states. These boilerplate codes are very annoying.

 1
2
3
4
5
6
7
8
 setI (
produce (i, async (draft) => {
setTimeout ( () => {
draft += 1
}, 0 )
return draft
}),
)

code using mobx

 1
2
3
4
 const store = useLocalStore ( () => ({ value : 0 }))
console . log (store. value ) // 0
store. value += 1
console . log (store. value ) // 1

The advantage of this model is that you can directly modify the state without using the set function, and you can directly read the latest value without using await to wait for the next cycle. Basically, it’s similar to vue’s reactive hooks, generating a mutable object that can then be modified and read, even deep. In a sense, vue3 hooks is indeed a simplification of react + mobx, but compared to jsx, templates make many people unaccustomed (dislike).

dependency hell

For example, the following code is very common in react

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 import { useState, useEffect } from 'react'

function App ( ) {
const [text, setText] = useState ( '' )
const [result, setResult] = useState ( '' )
useEffect ( () => {
fetch ( '/api?text=' + text)
. then ( ( response ) => response. text ())
. then ( ( data ) => {
setText (data)
})
}, [text])

return (
< div >
< input value = {text} onChange = {(e) => setText(e.target.value)} />
< div > {result} </ div >
</ div >
)
}

Using mobx can be rewritten as

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
 import { observer, useLocalStore } from 'mobx-react-lite'

const App = observer ( () => {
const store = useLocalStore ( () => ({
text : '' ,
result : '' ,
setText ( text: string ) {
this . text = text
fetch ( '/api?text=' + this . text )
. then ( ( response ) => response. text ())
. then ( ( data ) => {
this . result = data
})
},
}))

return (
< div >
< input
value = {store.text}
onChange = {(e) => store.setText(e.target.value)}
/>
< div > {store.result} </ div >
</ div >
)
})

However, in general, mobx may only manage state, and related functional functions are placed at the top level of components.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
 import { observer, useLocalStore, useObserver } from 'mobx-react-lite'

const App = observer ( () => {
const store = useLocalStore ( () => ({
text : '' ,
result : '' ,
}))

useObserver ( () => {
fetch ( '/api?text=' + store. text )
. then ( ( response ) => response. text ())
. then ( ( data ) => {
store. result = data
})
})

return (
< div >
< input
value = {store.text}
onChange = {(e) => (store.text = e.target.value)}
/>
< div > {store.result} </ div >
</ div >
)
})

The same is true for useMemo, which can be replaced by mobx’s computed. Similarly, it is automatically optimized.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
 import { observer, useLocalStore, useObserver } from 'mobx-react-lite'

const App = observer ( () => {
const store = useLocalStore ( () => ({
text : '' ,
result : '' ,
get computedResult () {
return this . result + this . text
},
}))

useObserver ( () => {
fetch ( '/api?text=' + store. text )
. then ( ( response ) => response. text ())
. then ( ( data ) => {
store. result = data
})
})

return (
< div >
< input
value = {store.text}
onChange = {(e) => (store.text = e.target.value)}
/>
< div > {store.computedResult} </ div >
</ div >
)
})

Encapsulate some tool hooks

Sure, mobx might have some boilerplate code, but that can be solved with some encapsulation, looks like vue hooks xd.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
30
31
32
33
34
35
36
37
 import { useLocalStore, useObserver } from 'mobx-react-lite'

/**
* 声明一个状态,一般用于原始值,例如数字或者字符串
*/
export function useLocalRef<T>( value : T): { value : T } {
return useLocalStore ( () => ({ value }))
}

/**
* 声明一个状态,一般用于非原始值,例如对象或者数组
*/
export function useLocalReactive<T extends Record < string , any >>( value : T): T {
return useLocalStore ( () => value)
}

/**
* 声明根据状态变更运行副作用
*/
export function useLocalWatchEffect ( f: () => void , dep?: () => any ) {
useObserver ( () => {
dep?.()
return f ()
})
}

/**
* 声明一个计算属性
*/
export function useLocalComputed<T>( f : () => T): { value : T } {
const r = useLocalStore ( () => ({
get value () {
return f ()
},
}))
return r
}

limitation

While mobx is good, it has some limitations

  • Requires some boilerplate code observer/useLocalStore
  • Subcomponents can modify the state passed in
  • ToJS is needed to convert the proxy proxy object into a normal js object during structured cloning
  • There is no direct explicit declaration of dependencies on methods running side effects
  • It is not possible to completely avoid using some methods that come with react hooks, especially when relying on some third-party libraries

This article is transferred from: https://blog.rxliuli.com/p/1fcd1517c14c4e68b764d7e6df071805/
This site is only for collection, and the copyright belongs to the original author.