Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value equality in selectors? #38

Open
suzdalnitski opened this issue Jul 26, 2022 · 1 comment
Open

Value equality in selectors? #38

suzdalnitski opened this issue Jul 26, 2022 · 1 comment

Comments

@suzdalnitski
Copy link

Hi, it seems that selectors will always cause a rerender if the reference changed (even though the value stays the same). Is there a way to use value equality instead?

Thanks!

@suzdalnitski
Copy link
Author

Looks like this isn't currently possible. But following a similar discussion in the main Recoil repo, I've rewritten the code that they provide as a solution in Rescript. Posting it here, in case somebody may find this helpful:

type equalAtomOptions<'value> = {
  key: string,
  default: 'value,
  equals: ('value, 'value) => bool,
}

/**
 * Use a writable selector to prevent excess renders.
 * If the setting value is equal to the current value, don't change anything.
 */
let equalAtom = (options: equalAtomOptions<'value>): Recoil.readWrite<'value> => {
  let {key, equals, default} = options

  let inner = Recoil.atom({
    key: `${options.key}_inner`,
    default: default,
  })

  Recoil.selectorWithWrite({
    key: key,
    get: ({get}) => get(inner),
    set: ({get, set}, newValue) => {
      let current = get(inner)

      if !equals(newValue, current) {
        set(inner, _ => newValue)
      }
    },
  })
}

type equalSelectorOptions<'value> = {
  key: string,
  get: Recoil.getValue<'value>,
  equals: ('value, 'value) => bool,
}

/**
 * Use a wrapper selector to prevent excess renders.
 * If the latest selection is value-equal to prior ref, return the prior ref.
 */
let equalSelector = (options: equalSelectorOptions<'value>): Recoil.readOnly<'value> => {
  let inner = Recoil.selector({
    key: `${options.key}_inner`,
    get: options.get,
  })

  let prior = ref(None)

  Recoil.selector({
    key: options.key,
    get: ({get}) => {
      let latest = get(inner)

      switch prior.contents {
      | Some(prior) if options.equals(latest, prior) => prior
      | _ =>
        prior := Some(latest)
        latest
      }
    },
  })
}

type equalSelectorFamilyOptions<'parameter, 'value, 'paramIdentity> = {
  key: string,
  get: 'parameter => Recoil.fn<Recoil.getValue<'value>>,
  equals: ('value, 'value) => bool,
  emptyParamMap: Belt.Map.t<'parameter, 'value, 'paramIdentity>,
}

/**
 * Use a wrapper selector to prevent excess renders.
 * If the latest selection is value-equal to prior ref, return the prior ref.
 */
let equalSelectorFamily = (
  options: equalSelectorFamilyOptions<'parameter, 'value, 'paramIdentity>,
) => {
  let inner_key = `${options.key}_inner`

  let inner = Recoil.selectorFamily({
    key: inner_key,
    get: options.get,
  })

  let priors = ref(options.emptyParamMap)

  Recoil.selectorFamily({
    key: options.key,
    get: (param: 'parameter) => Fn(
      ({get}) => {
        let innerValue = inner(param)
        let latest = get(innerValue)
        let prior = Belt.Map.get(priors.contents, param)

        switch prior {
        | Some(prior) if options.equals(latest, prior) =>
          prior
        | _ =>
          priors := Belt.Map.set(priors.contents, param, latest)

          latest
        }
      },
    ),
  })
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant