saasquatch / bunshi

Molecule pattern for jotai, valtio, zustand, nanostores, xstate, react and vue
https://www.bunshi.org/
MIT License
230 stars 17 forks source link

Support for `initialValues` #9

Closed mutewinter closed 2 years ago

mutewinter commented 2 years ago

I am wanting to initialize the values for my scoped atoms from props of a parent component. Right now, I'm doing it with a useSetAtom on every render in my ScopeProvider wrapper. It'd be nice if ScopeProvider could take initialValues like https://jotai.org/docs/api/core#provider.

loganvolkers commented 2 years ago

The pattern that we've used to solve this is to put those values into scope, here's an example:

In this case, we use the values from this molecule to create derived atoms, but you can do the same thing with atom initial values.

import { atom, Atom, PrimitiveAtom } from 'jotai';
import { molecule, createScope, ScopeProvider } from 'jotai-molecules';
import React from 'react';
import type { AttributeConfig } from '../attributes/AttributeConfig';
import type { CanvasConfig } from '../canvas/CanvasConfig';
import type { Module } from '../component-metamodel/types';

export type RaisinConfig = Partial<CanvasConfig> &
  Partial<AttributeConfig> & {
    /**
     * Atom for the primitive string value that will be read
     */
    HTMLAtom: PrimitiveAtom<string>;
    /**
     * Atom for the set of NPM packages
     */
    PackagesAtom: PrimitiveAtom<Module[]>;
    /**
     * Atom for the set of UI Widgets that can be use for editing attributes
     *
     * Read-only
     */
    uiWidgetsAtom: Atom<Record<string, React.FC>>;
    /**
     * When an NPM package is just `@local` then it is loaded from this URL
     *
     * Read-only
     */
    LocalURLAtom: Atom<string | undefined>;
  };

type Molecule<T> = ReturnType<typeof molecule>;
/**
 * The core thing that needs to be provided to raisins for editing to be possible.
 *
 * Everything in raisins is in some way *derived state* from this molecule
 */
export type RaisinConfigMolecule = Molecule<Partial<RaisinConfig>>;

const configScope = createScope<RaisinConfigMolecule | undefined>(undefined);
configScope.displayName = 'ConfigScope';

export const ConfigMolecule = molecule<RaisinConfig>((getMol, getScope) => {
  /**
   * This will create a new set of atoms for every {@link configScope}
   */
  const config = getScope(configScope);
  if (!config)
    throw new Error(
      'Must use this molecule in a wrapping <RaisinsProvider> element'
    );

  const LocalURLAtom = atom<string | undefined>(undefined);
  LocalURLAtom.debugLabel = 'LocalURLAtom';

  const HTMLAtom = atom('');
  HTMLAtom.debugLabel = 'HTMLAtom';

  const provided = getMol(config) as Partial<RaisinConfig>;

  return {
    ...provided,
    LocalURLAtom: provided.LocalURLAtom ?? atom(undefined),
    PackagesAtom: provided.PackagesAtom ?? atom<Module[]>([]),
    uiWidgetsAtom: provided.uiWidgetsAtom ?? atom({}),
    HTMLAtom: provided.HTMLAtom ?? HTMLAtom,
  };
});

/**
 * Provides a scope for editing a {@link RaisinConfigMolecule}
 */
export const ConfigScopeProvider = ({
  molecule,
  children,
}: {
  molecule: RaisinConfigMolecule;
  children: React.ReactNode;
}) => {
  return (
    <ScopeProvider scope={configScope} value={molecule}>
      {children}
    </ScopeProvider>
  );
};

export const RaisinsProvider = ConfigScopeProvider;
mutewinter commented 2 years ago

Ah didn't realize the value could be a non-string. I see the example of using an atom for the scope value in ScopeProvider.test.tsx now. It's a little tedious due to needing to memoize the entire object for shallow equality, but perhaps that's the only way to ensure initialization re-runs if values change.