skip to Main Content

I have a MobX Store that contains user role information and a method that validates the role:

class Store {
    constructor() {
        makeAutoObservable(this)
    }

    role: string = ""

    setRole(name: string) {
        this.role = name
    }

    checkRole(name: string) {
        return this.role === name
    }
}

I want to use the checkRole method inside useMemo in my component, in order not to recalculate the value if the role has not changed in any way. (I realize that this example is a simple calculation and useMemo is not needed, but it’s just an example)

My component looks like this:

const App = observer(() => {
    const [, forceRender] = useReducer((prev) => prev + 1, 0)
    const store = useContext(StoreContext)

    const isHaveAccess = useMemo(() => {
        return store.checkRole('ADMIN')
    }, [store, store.role])

    return (
        <div>
            {isHaveAccess ? 'Access allowed' : 'Access denied'}
            <button
                onClick={() => store.setRole('ADMIN')}
            >Set Role</button>
            <button onClick={forceRender}>Force Render</button>
        </div>
    );
})

It works, but I get a warning from ESLint: React Hook useMemo has an unnecessary dependency: 'store.role', which says that store.role is not used inside useMemo.

Also, if I move the logic from the checkRole method to useMemo, the warning disappears.

Is this a normal practice or is there a better way to do it?

I tried using computed with arguments, but that didn’t solve the problem and the checkRole method was called on every render.

2

Answers


  1. Chosen as BEST ANSWER

    After some time, I came up with another solution to the problem. It consists in using computed inside useMemo. We can write the hook useComputed which is a wrapper over useMemo and computed, it looks like this:

    const useComputed = <T,>(factory: () => T, deps?: DependencyList): T => {
        return useMemo(() => computed(factory), deps || []).get()
    }
    
    const App = observer(() => {
        const [, forceRender] = useReducer((prev) => prev + 1, 0)
        const store = useContext(StoreContext)
    
        const isHaveAccess = useComputed(() => {
            return store.checkRole('ADMIN')
        }, [store])
    
        return (
            <div>
                {isHaveAccess ? 'Access allowed' : 'Access denied'}
                <button
                    onClick={() => store.setRole('ADMIN')}
                >Set Right</button>
                <button onClick={forceRender}>Force Render</button>
            </div>
        );
    })
    

    This works because useMemo returns a previous reference to computed function, so when calling get(), mobx can return a cached value if the observed data has not changed.


  2. You can just suppress this eslint warning in that case and keep the code as is. The downside is that your code is not very transparent that way, basically useMemo somehow knows the internal implementation of checkRole, that it needs store.role to recalculate. Imagine that you change implementation in the future and it will be easy to forget to add new deps to the memo call, or remove extra. It’s a bit dirty, but it’s more or less normal practice if you are fine with it being dirty.

    I would suggest you to try computedFn from mobx-utils, it’s basically exactly what you need. Be careful though, it needs to be almost pure and only rely on observables and function arguments.

    //...
    
    checkRole = computedFn((name) => this.role === name)
    
    // ...
    

    Another option is to make checkRole pure and just pass both current and target role all the time, but it’s a bit more verbose. eslint will be happy though.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search