npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

relude-reason-react

v0.10.0

Published

Relude-based utilities for ReasonReact

Downloads

89

Readme

relude-reason-react

GitHub CI npm

Overview

Relude-based utilities for ReasonReact

Documentation

ReludeReact.Reducer.useReducer hook

The ReludeReact.Reducer.useReducer hook was inspired by the original/pre-hooks ReasonReact record API, and the hooks-based reason-react-update libray by Matthias Le Brun (bloodyowl).

ReludeReact.Reducer.useReducer is similar to the React useReducer hook with the key difference that the React useReducer only allows you to change the state, whereas the ReludeReact.Reducer.useReducer allows you to both change the state, and to safely emit side effects or Relude.IO-based actions, which can result in the emission of further actions.

To use the ReludeReact.Reducer.useReducer hook, you must provide a reducer function of the following type:

type reducer('action, 'state) = ('state, 'action) => update('action, 'state);

A function that accepts the current 'state and an 'action, and returns a value of type update('action, 'state)

The update value is a variant with the following type:

type update('action, 'state) =
  | NoUpdate
  | Update('state)
  | UpdateWithSideEffect('state, SideEffect.Uncancelable.t('action, 'state))
  | SideEffect(SideEffect.Uncancelable.t('action, 'state))
  | UpdateWithCancelableSideEffect(
      'state,
      SideEffect.Cancelable.t('action, 'state),
    )
  | CancelableSideEffect(SideEffect.Cancelable.t('action, 'state))
  | UpdateWithIO('state, SideEffect.Uncancelable.IO.t('action, 'state))
  | IO(SideEffect.Uncancelable.IO.t('action, 'state));

Basically, this means that in your ReludeReact.useReducer component, for any action that occurs, you can respond to the action by doing any of the following things:

NoUpdate

Don't change the state, and don't perform an side effects or IO-based effects. Basically a no-op - useful as a placeholder or to stub out actions prior to providing the implementations.

Update(state)

Update the component state to the given value, but don't perform any side effects or IO-based effects.

UpdateWithSideEffect(state, {state, send} => unit)

Update the component state to the given value, and perform the given side effect (basically a function that is given a context record of state and send and is allowed to perform any type of sync or async side effect, and emit additional actions via send, which is a function of type action => unit, and ultimately return unit (). In this case, the side effect is Uncancelable, which means, there is no way to cancel it later.

These types of side effects are useful for doing things like pushing a history state to navigate to a different URL, doing one-off DOM manipulations, or other types of things you don't want or need to manage or control.

SideEffect({state, send} => unit)

Same as UpdateWithSideEffect, but with no state update.

UpdateWithCancelableSideEffect(state, {state, send} => (unit => unit))

Same as UpdateWithSideEffect, but the side effect can be cancelled via a returned canceler function.

CancelableSideEffect({state, send} => (unit, unit))

Same as UpdateWithCancelableSideEffect, but with no state update.

UpdateWithIO(state, Relude.IO.t(action, action))

Similar to UpdateWithSideEffect, but instead of a function that accepts the side effect context and returns (), you return a Relude.IO.t('action, 'action). An IO is a data type which can perform any type of synchronous or asynchronous side effect - see below. Relude.IO is a bi-functior which has a typed error channel, and a typed "success" channel. In this case the success and error channels are both constrained to the type 'action, which means that your IO, when executed, must produce an 'action to dispatch on either success or failure.

A common pattern with component actions is to perform some async action (which typically can fail, e.g. an AJAX/fetch call), and then send a new 'action when the async invocation either succeeds or fails. This patterns is exactly what's captured by the Relude.IO.t('action, 'action) type. See below for a more illustrative example.

IO(Relude.IO.t(action, action))

Similar to UpdateWithIO, but with no initial state update.

Relude.IO Aside

Relude.IO is a data type that can be used to execute side effects in a purely functional way. For those coming from the JavaScript world, you can think of IO as something similar to a lazy promise, but with lots of extra capabilities, brought to you by the power of math and functional programming.

Using IO rather than ad-hoc side effect functions gives you all sorts of useful functions for mapping/flatMapping results and errors, catching and transforming errors, combining multiple async results, and so much more.

See Relude IO documentation for more information.

ReludeReact.Effect hooks

useOnMount

ReludeReact.useOnMount is a simple shortcut which allows you to register a simple unit => unit function to run when a component is first mounted. This is typically used to send an initial 'action into your reducer for initializing the component (e.g. fetch any initial data).

useIOOnMount hook

ReludeReact.Effect.useIOOnMount (and it's variations) allows you to trigger a Relude.IO-based action when the component is mounted, and handle the final resulting value (either success or failure) using a side-effect callback.

This could be useful if you need to dispatch a fetch request on mount, and then dispatch some reducer actions on success and/or failure, or if you need to store the result of the fetch request in localStorage, etc.

Variations of this function exist which allow different types of result callbacks - i.e. a callback from Belt.Result.t('a, 'e) => unit, separate 'a => unit and 'e => unit callbacks, etc.

useEffect1WithEq...useEffect5WithEq

These effect hooks are very similar to their React.useEffectN counterparts, except that you provide your own equality function along with any values the hook depends upon for re-running.

React's useEffect dependencies are simply checked by (===), which is fast but may lead to false positives when deciding if a hook dependencies have changed (particularly with complex types like records and lists). In cases where running an effect may be expensive, useEffectNWithEq allows much more control over whether that effect should run.

ReludeReact.Render utilities

ReludeReact.Render contains a variety of useful functions for rendering different data types, to avoid extra boilerplate/noise in your components. The purpose of these functions is to try to streamline conditional rendering, so you don't have to write lots of _ => React.null cases when rendering conditional values, variants like Relude.AsyncResult.t('a, 'e), etc.

ReludeReact.Render.ifTrue
ReludeReact.Render.ifTrueLazy
ReludeReact.Render.ifFalse
ReludeReact.Render.ifFalseLazy
ReludeReact.Render.option
ReludeReact.Render.optionLazy
ReludeReact.Render.optionIfSome
ReludeReact.Render.result
ReludeReact.Render.resultIfOk
ReludeReact.Render.resultIfError
ReludeReact.Render.asyncData
ReludeReact.Render.asyncDataLazy
ReludeReact.Render.asyncDataByValue
ReludeReact.Render.asyncDataLazyByValue
ReludeReact.Render.asyncResult
ReludeReact.Render.asyncResultLazy
ReludeReact.Render.asyncResultByValue
ReludeReact.Render.asyncResultLazyByValue
// And many more!

Examples

See the demo app in examples/demo

> npm run demo

Below is a somewhat contrived/simple example of what a ReludeReact component might look like.

// AnimalListView.re
open Relude.Globals;

// The state of this component
// We're using a Relude.AsyncResult to represent the state of animals, which are loaded asynchronously and can fail.
type state = {
  title: string,
  animalsResult: AsyncResult.t(list(Animal.t), Error.t),
};

// The initial state for the component (used in the reducer initialization below)
let initialState = {title: "Animals", animalsResult: AsyncResult.init};

// The actions that our component emits and handles in the reducer
type action =
  | FetchAnimals
  | FetchAnimalsSuccess(list(Animal.t))
  | FetchAnimalsError(Error.t)
  | ViewCreateForm
  | ViewAnimal(Animal.t)
  | DeleteAnimal(Animal.t)
  | NoOp;

// The reducer function which accepts and action and the current state, and emits
// an "update" which can do things like updating the state, running raw or IO-based effects
let reducer =
    (state: state, action: action): ReludeReact.Reducer.update(action, state) =>
  switch (action) {
  | FetchAnimals =>
    UpdateWithIO(
      {...state, animalsResult: state.animalsResult |> AsyncResult.toBusy},
      API.fetchAnimals
      |> IO.bimap(a => FetchAnimalsSuccess(a), e => FetchAnimalsError(e)),
    )

  | FetchAnimalsSuccess(animals) =>
    Update({...state, animalsResult: AsyncResult.completeOk(animals)})

  | FetchAnimalsError(error) =>
    Update({...state, animalsResult: AsyncResult.completeError(error)})

  | ViewCreateForm => SideEffect(_ => ReasonReactRouter.push("/create"))

  | ViewAnimal(_animal) => NoUpdate

  | DeleteAnimal(_animal) => NoUpdate

  | NoOp => NoUpdate
  };

// Various inline components

module AnimalsLoading = {
  [@react.component]
  let make = () => {
    <div> {React.string("Loading animals...")} </div>;
  };
};

module AnimalsTable = {
  [@react.component]
  let make = (~animals: list(Animal.t), ~send: action => unit) => {
    let _ = send; // TODO
    <div>
      {React.string("Animals: " ++ string_of_int(List.length(animals)))}
    </div>;
  };
};

module AnimalsError = {
  [@react.component]
  let make = (~error: Error.t) =>
    <div> {React.string(Error.show(error))} </div>;
};

module AnimalsResult = {
  [@react.component]
  let make = (~result: AsyncResult.t(list(Animal.t), Error.t), ~send) =>
    result
    |> ReludeReact.Render.asyncResultByValueLazy(
         _ => <AnimalsLoading />,
         animals => <AnimalsTable animals send />,
         error => <AnimalsError error />,
       );
};

// The main view - accepts the state and send values we get from the reducer

module Main = {
  [@react.component]
  let make = (~state, ~send) => {
    <div>
      <h1> {React.string(state.title)} </h1>
      <div>
        <button
          onClick={e => {
            ReactEvent.Synthetic.preventDefault(e);
            send(ViewCreateForm);
          }}>
          {React.string("Create")}
        </button>
        <button
          href="#"
          onClick={e => {
            ReactEvent.Synthetic.preventDefault(e);
            send(NoOp);
          }}>
          {React.string("No-Op Action")}
        </button>
      </div>
      <AnimalsResult send result={state.animalsResult} />
    </div>;
  };
};

// The main component definition
// Here, we invoke our hooks and render the main view

[@react.component]
let make = () => {
  // Initialize the ReludeReact reducer
  let (state, send) = ReludeReact.Reducer.useReducer(reducer, initialState);

  // Trigger an initialization action on mount
  // This is just using the send function from our reducer to send an action,
  // which is handled by the reducer
  ReludeReact.Effect.useOnMount(() => send(FetchAnimals));

  // This is just demonstrating triggering an IO action on mount, and handling
  // the result via side-effecting functions
  // In reality, the IO would probably be making a fetch request, or doing some
  // other async action and then storing or dispatching the results.
  ReludeReact.Effect.useIOOnMount(
    IO.suspend(() => {
      Js.log("Suspend 42");
      42;
    }),
    intValue => Js.log("Got suspended value: " ++ string_of_int(intValue)),
    _error => Js.log("Suspend 42 failed"),
  );

  // This just demonstrates that with our special effect hooks, you can provide
  // a custom EQ function that will prevent a hook from running even if React's
  // basic (===) check thinks the value has changed
  ReludeReact.Effect.useEffect1WithEq(
    () => Js.log("Running effect because some array has changed!"),
    (a, b) => List.String.(eq(sort(a), sort(b))),
    List.shuffle(["a", "b", "c", "d"]),
  );

  // Render our main view, passing the state and dispatcher function down
  <Main state send />;
};

Developer info

Project setup

> git clone [email protected]:reazen/relude-reason-react
> cd relude-reason-react
> npm install
> npm run server:demo

Scripts

> npm run clean
> npm run build
> npm run cleanbuild
> npm run test
> npm run cleantest
> npm run watch
> npm run demo

Publishing to npm

> npm version major|minor|patch
> git push origin --follow-tags
> git push upstream --follow-tags
> npm publish

NixOS

If you have trouble building/installing the Bucklescript/Reason tools try this:

> nix-shell
%nix%> npm install