Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/tokland/omreact
Purely functional React components with local state
https://github.com/tokland/omreact
async components elm functional immutable local-state react reducer state
Last synced: about 1 month ago
JSON representation
Purely functional React components with local state
- Host: GitHub
- URL: https://github.com/tokland/omreact
- Owner: tokland
- License: mit
- Created: 2018-08-25T16:14:29.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2018-09-24T18:28:10.000Z (over 6 years ago)
- Last Synced: 2023-04-18T21:50:51.432Z (over 1 year ago)
- Topics: async, components, elm, functional, immutable, local-state, react, reducer, state
- Language: JavaScript
- Size: 934 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
## Purely functional React components with local state
`OmReact` is a thin layer over React that allows writing purely functional components that hold local state. React is mostly a functional framework, but it still promotes imperative code through `this.setState`, which works by performing side-effects.
The idea is similar to the [Elm architecture](https://guide.elm-lang.org/architecture/), but applied to components: define a **single update** function that takes **events** and returns **actions** (new state + async action + parent prop calls). On render, event props take pure values, either event constructors or plain values (example: `$onClick="increment"`), instead of functions with side effects like typical React components do.
## Install
```sh
$ yarn add omreact
```## Example: a counter
```js
import React from 'react';
import {component, newState} from 'omreact';const init = newState({value: 0});
const update = (event, state, props) => {
switch (event) {
case "decrement":
return newState({value: state.value - 1});
case "increment":
return newState({value: state.value + 1});
default:
throw new Error(`Unknown event: ${event}`);
}
};const render = (state, props) => (
-1
+1
{state.value}
);export default component("MyCounter", {init, render, update});
```## OnReact component
### Component overview
![Diagram](https://github.com/tokland/omreact/blob/master/OmReact.png)
```js
type Action = StateAction | AsyncAction | ParentAction;component: (
name: string,
options: {
init: Action | Props => Action,
update: (Event, State, Props) => Action | Array,
render: (State, Props) => React.Element,
lifecycles?: {newProps?: (prevProps: Props) => Event},
propTypes?: Object,
defaultProps?: Object,
}) => React.Component;
```Options:
- `init`: Set the initial state and async/parent actions. This replaces `state =` in a React class component and async and props calls in `componentDidMount`.
- `update`: Takes an event, the current `state` and `props`, and returns an action or actions to dispatch.
- `render` with `$eventProp={Event | Args => Event}`: Like a React `render` function except that event props must be prefixed with a `$`. An event can be either a plain value or a pure function. `$` is being used for convenience, it's a valid character for a variable name so there is no need to use a custom JSX babel transform. `@onClick={...}` would be probably nicer, though.
- `lifecycles`: More on the lifecyle section.
- `propTypes`/`defaultProps`. Standard React keys, will be passed down to the component.
### Actions
#### Update state (`newState`)
Return the new state of the component. This should be the new full state, not partial state like `this.setState` takes. Function: `newState: State => Action` is provided.
#### Async side-effects (`asyncAction`)
In `OmReact` components, you don't have access to `setState`, to write asynchronous code (timers, requests), you return instead an async action with a promise that resolves into some other actions. An example:
```js
import React from 'react';
import {Button} from '../helpers';
import {component, asyncAction, newState} from 'omreact';const getRandomNumber = (min, max) => {
return fetch("https://qrng.anu.edu.au/API/jsonI.php?length=1&type=uint16")
.then(res => res.json())
.then(json => (json.data[0] % (max - min + 1)) + min);
};const events = {
add: value => ({type: "add", value}),
fetchRandom: {type: "fetchRandom"},
};const init = newState({value: 0});
const update = (event, state, props) => {
switch (event.type) {
case "add":
return newState({value: state.value + event.value});
case "fetchRandom":
return asyncAction(getRandomNumber(1, 10).then(events.add));
default:
throw new Error(`Unknown event: ${JSON.stringify(event)}`);
}
};const render = (state, props) => (
+ASYNC_RANDOM(1..10)
{state.value}
);export default component("CounterWithSideEffects", {init, render, update});
```#### Call the parent component (`parentAction`)
React components report to their parents through props. While there is nothing preventing you from directly calling a prop in an `OmReact` component like you do in React, you should keep it purely functional by returning a a `parentAction`. Example:
```js
import React from 'react';
import {Button} from '../helpers';
import {component, newState, parentAction} from 'omreact';const events = {
increment: ev => ({type: "increment"}),
notifyParent: ev => ({type: "notifyParent"}),
};const init = newState({value: 0});
const update = (event, state, props) => {
switch (event.type) {
case "increment":
return newState({value: state.value + 1});
case "notifyParent":
return parentAction(props.onFinish, state.value);
default:
throw new Error(`Unknown event: ${JSON.stringify(event)}`);
}
};const render = (state, props) => (
+1
Notify parent
{state.value}
);export default component("CounterParentNotifications", {init, render, update});
```### Component Lifecycle
`OmReact` implements those React lifecycles:
* `newProps: (prevProps: Props) => Event`. Called any time props change.
Example:
```js
const events = {
newProps: prevProps => ({type: "newProps", prevProps}),
}const update = (event, state, props) => (
switch (event.type) {
case "newProps":
return newState({value: event.prevProps.value});
}
);export default component("MyCounterWithPropsChangeDetection",
{init, render, update, lifecycles: {newProps: events.newProps}});
```### Events
#### Typical event signatures
A typical way of defining events is to have *constructor arguments* (optional, should be memoized), *event arguments* (should not be memoized), or both. A typical `events` object may look like this:
```js
import {memoize} from 'omreact';const events = {
increment: {type: "increment"},
add: memoize(value => ({type: "add", value})),
addMouseButton: ev => ({type: "addMouseButton", ev}),
addValueAndMouseButton: memoize(value => ev => ({type: "add", value, ev})),
}
```Use like this on the event props of rendered elements:
- `events.increment`: An _object_, use it when you need no arguments. Example `$onClick={events.increment}`. The dispatcher will see that it's not a function and won't call it with the event arguments.
- `events.add`: A _1-time callable function_ that takes only event constructor arguments. Example: `$onClick={events.add(1)}`. This function should be memoized.
- `events.addMouseButton`: A _1-time callable function_ that takes only event arguments: Example: `$onClick={events.addMouseButton}`. This function should not be memoized.
- `events.addValueAndMouseButton`: A _2-time callable function_ that takes both constructor and event arguments: `$onClick={events.addValueAndMouseButton(1)}`. The first function should be memoized.#### Memoize events
One should not pass newly created values as props to components, as React will think those props changed and will issue an unnecessary re-render. This applies to arrays, objects or arrow functions - no problem with strings or numbers, `===` works fine on them. So we should extract prop to `const` values . Also, use memoization, the library already provides a helper for that: `memoize`, in event constructors. Example:
```js
import {component, memoize} from 'omreact';const events = {
increment: ev => {type: "increment"},
add: memoize(value => ({type: "add", value})), // Use: $onClick={events.add(5)}
};
```#### Events are agnostic
An event can be any any object or function (if it has constructor/prop arguments). Create your own abstractions using strings, arrays, objects, [proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), whatever.
Check the [examples](examples/src) to see some alternative ways:
- Using a [function constructor](https://github.com/tokland/omreact/blob/master/examples/src/counter/CounterUsingEventFunctionCreator.js).
- Using [ADT constructors](https://github.com/tokland/omreact/blob/master/examples/src/counter/CounterSimpleAdt.js).
- Using on-the-fly [proxy constructors](https://github.com/tokland/omreact/blob/master/examples/src/counter/CounterEventWithProxy.js).
#### Events are composable
```js
import {component, newState, composeEvents, memoize} from 'omreact';// ...
const update = (event, state, props) => event.match({
add: value =>
newState({value: state.value + value}),
addOnePlusTwo: () =>
composeEvents([events.add(1), events.add(2)], update, state, props),
});// ...
```## Examples
Check the [examples](examples/src) directory in the repository.
```sh
$ cd examples
$ yarn install
$ yarn start
```