15

I feel like I'm getting most of mobx, but I want to clarify something. I've got a store with some observables, the core of which is an array of objects (typescript):

class ClientStore {
    constructor() {
        this.loadClients();
    }

    @observable private _clients: IClient[] = [];
    @computed get clients() {
        return this._clients;
    }
    @observable isFetching: boolean = false;
    @observable sortField: 'title' | 'modified' = 'modified';
    @observable sortDescending: boolean = true;

    getClientByUrlName(urlName: string): IClient {
        return this._clients.find(c => c.urlName === urlName);
    }
etc...

My question is that last function - getClientByUrlName. Since that's finding from an observable, any @observer react component that's using that function re-renders correctly. Is this idiomatic mobx for something like that? It feels like that should be a computed value. Should I make is a computed value in the component that wants to use it?

//import singletone ClientStore
class ClientDetailsView extends React.Component<{params:{urlName:string}}, void> {

    @computed get client() {
        return ClientSotre.clients.find(c => c.urlName === this.props.params.urlName);
    }

...rest of react component

I'm looking for best practices and gotchas here. Any help is appreciated.

*edit fixed code sample error

Jakke
  • 287
  • 1
  • 2
  • 11

2 Answers2

19

In principle @computed is simple a directive that tells MobX: "this value could be cached until any of the observables that is used changes". So in fact they can always be left out, it will just mean that your app will recompute more, but it doesn't alter the results.

So if you are missing @computed on the function that is in principle not a problem in most cases. If it is, you can use createTransformer which takes a one-argument function and builds a (self cleaning) memoization cache of computed values. But it is a little bit more involved, so actually your solution to introduce a computed property in your ClientDetailsView is nicer. I would indeed recommend doing that as long as you have a nice place to put that computed property (the component that needs it in this case)

mweststrate
  • 4,547
  • 1
  • 14
  • 22
  • 3
    Ah hah! That seemed like a piece I was missing. Every time I go over the docs I see a part as if it was brand new. Funny that sticking the computed property in the component itself came to me as I was writing the question. [off topic] I'm really like MobX in general by the way (I'm converting from redux). The more I learn how it works the more I like it. Thanks for all the hard work! – Jakke Jun 28 '16 at 19:30
  • I actually did restructure the docs the last days a bit, so that impression is right :) – mweststrate Jun 29 '16 at 06:57
  • 1
    I am a little confused why it would be better to put the `@computed` in the ClientDetailsView instead of in the store. Would putting it in the store have the same effect? – Jeff Jun 29 '16 at 19:58
  • Getters can't have parameters, @computed has to be a getter, and the store itsn't aware of which urlName I'm looking for. On a higher level it makes sense that the store can't compute and memoize a value that it doesn't have enough information to get. The real solution is the createTransformer mentioned in the answer, as that makes the store memoize the value each time a new parameter is passed in, but in cases where it's overkill my solution feels simpler. I actually like it for mine, as many components may access this value, so I probably will be going the transformer route. – Jakke Jun 30 '16 at 09:48
  • @mweststrate actually this example explained it very well:https://github.com/mobxjs/mobx/issues/101#issuecomment-220891704 , Thanks a lot. – ProllyGeek Feb 23 '17 at 11:38
1

For anyone wondering how to use Computed in Functional Components.

You have to combine useMemo() from react and computed() from mobx:

TypeScript

import {useMemo} from 'react';
import {computed, IComputedValueOptions} from 'mobx';

// changes to "options" argument are ignored
export default function useComputed<T>(func: () => T, options?: IComputedValueOptions<T>, deps?: DependencyList)
{
    return useMemo(() => computed(func, options), deps ?? []).get();
}

JavaScript

import {useMemo} from 'react';
import {computed} from 'mobx';

// changes to "options" argument are ignored
export default function useComputed(func, options)
{
    return useMemo(() => computed(func, options), deps ?? []).get();
}

Example

export default observer(function MyComponent()
{
    const sum = useComputed(() => observableA.myValue + observableB.myValue);
    return <div>{sum}</div>
})

Props should be in dependencies

export default observer(function MyComponent({observableA, observableB})
{
    const sum = useComputed(() => observableA.myValue + observableB.myValue, null, [observableA, observableB]);
    return <div>{sum}</div>
})

If you don't put props in deps then computed() will continue using old props and it will never update.

Don't forget to wrap your components with observer() or it wont react to changes!

zoran404
  • 1,090
  • 14
  • 30