A React microframework for pure state management and managed side effects. Inspired by the Elm architecture, no Redux needed. No dependencies other than React itself.
This is currently in the beta phase! After gathering some feedback it will be bumped to stable soon.
- ✅ all-in-one: State management and effect handling out of the box.
- ✅ no redux libraries needed: No redux, react-redux, redux-thunk, redux-sage, redux-loop etc needed.
- ✅ zero dependencies: Has no dependencies other than React itself.
- ✅ lightweight: The implementation is in a single file of ~1KB minified and a bit over ~100 lines of code including comments. (Check on bundlephobia)
- ✅ typed: This library ships with TypeScript Types.
React is universally used, but leaves a lot of open questions regarding how to manage application state and side-effects.
For this reason a multitude of libraries like redux
, redux-thunk
, redux-saga
etc have emerged, which all come with a dependency footprint, boiler plate code, and up front planning on which libraries to select.
Wouldn't it be nice to have a simple setup that works for (almost) all cases with a single dependency and minimal boiler plate?
So this library is for you if
- you like to have your application state and side-effects managed in a clean way
- you're tired of wiring together multiple (redux-)libraries
- wanna keep dependencies to a minimum
- keep your app simple as it grows
This pattern (also known as the Elm architecture) breaks down an app into 4 main functions.
function | description |
---|---|
init | Initialize your app by returning a tuple of your initial model and any effects you want to run. Runs only once when createApp() is called. |
update | A function that gets messages, the current model and computes and returns a new model (reducer) and a list of effects. Runs whenever messages are send (via sendMsg ). |
view | A function that gets the model and a message dispatching function and returns some JSX. Called whenever update() has run. |
subscriptions | A function that sets up and removes event listeners to events external to the application like timers, sockets, or clicks on the document and dispatches new messages. Runs whenever the model changes. |
npm i -D react-model-update-view
See the examples/
folder.
import React from "react";
import ReactDOM from "react-dom/client";
// eslint-disable import/no-unresolved
import { createApp, useSendMsg } from "react-model-update-view";
function documentClickSubscription(sendMsg) {
const listener = () => sendMsg({ type: "documentClick" });
document.addEventListener("click", listener);
return () => {
// return unsubscribe function
document.removeEventListener("click", listener);
};
}
function logEffect(text) {
return (sendMsg) => console.log(text);
}
const App = createApp({
init() {
return [0, []];
},
update(msg, model) {
console.log({ msg });
switch (msg.type) {
case "plus":
return [model + 1, [logEffect("plus")]];
case "minus":
return [model - 1, [logEffect("minus")]];
case "reset":
return [0, []];
case "documentClick":
return [model + 5, []];
default:
throw new Error(`Unknown msg "${msg.type}"`);
}
},
view(model, sendMsg) {
return (
<div>
<h2> {model}</h2>
<button type="button" onClick={() => sendMsg({ type: "plus" })}>
+
</button>
<button type="button" onClick={() => sendMsg({ type: "minus" })}>
-
</button>
<ResetButton />
</div>
);
},
subscriptions(model) {
// listen to document clicks, unsubscribe if model is >= 30
return model < 30 ? [documentClickSubscription] : [];
},
});
function ResetButton() {
const sendMsg = useSendMsg();
return (
<button type="button" onClick={() => sendMsg({ type: "reset" })}>
Reset
</button>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
If you're familiar with redux or React's useReducer
hook, those are different names for things you already know.
model
->state
msg
->action
sendMsg()
->dispatch()
They are conceptually the same thing. Feel free to use the names you are comfortable with in your app.
Extra hint 💡: update()
would be similar to your reducer()
. 😉
Yes, createApp
has a generic type signature. So you can use it like this in Typescript.
const App = createApp<Model, Msg>({ init, update, view, subscriptions });
Yes, absolutely. Returning the effects from update()
doesn't violate this though, because update()
simply returns them.
The effects a are executed by the framework NOT in the update()
function itself. This makes it safe to run the update()
function and easy to test.
Here are some effect examples and subscriptions examples.
Yes, everything in the view()
function is still plain react, so hooks etc are available.
We recommend to put all application state into the model and have it managed by update()
as single source of truth.
However, there may be component state, that is UI based and not as important to your app as a whole, such as whether something is focused, or has been clicked before. Those are things where it's up to you to manage it as a local component state, but the general recommendation is to keep that to a minimum.
Having to pass down values through a deep component hierarchy can be annoying and bloat your code.
You'll likely need to be able to trigger messages from many places in your app. To avoid having to pass around sendMsg()
everywhere,
the useSendMsg
hook will give you easy access to the message triggering function from anywhere (not needed in directly in view()
though because
view()
receives sendMsg()
as the second argument)
There is no such hook for accessing the model, because usually with good model design, data is passed down to components quite naturally. If you find yourself struggling with that, please open an issue.
The functions are best tested in isolation.
It's best to run the init function and assert the expected model. The effects
// init
const [model, effects] = init();
expect(model).toEqual(expectedModel);
// update
const [newModel, effects] = update({ type: "someMsg" }, someModel);
expect(newModel).toEqual(expectedModel);
Effects are a bit harder to test, since they are not just plain input/output. For many testing the model is enough for others this propably requires mocking.
To test the output of the view function you probably wanna use something like the React Testing library.
Also a bit harder to test because this requires side-effects. This probably also requires mocking. If you keep the subscriptions simple you can probably get by without tests.
For the complete docs check the API docs.
createApp({ init, update, view, subscriptions })
: Create an app. This function returns a React component you can use in your JSX as a top level element or child.useSendMsg()
: A hook to get thesendMsg()
function, to triggerupdate()
. It's mainly for convenience, so you don't have to pass downsendMsg()
in deep component hierarchies. Think ofsendMsg()
likedispatch()
.
Many languages, libraries and framworks have influenced this library. Here is a quick overview over the most relevant ones. Thanks to all ❤️
- The Elm programming language and its Elm architecture
- Redux as a state management solution
- Redux loop: A redux middleware aiming to port Model-Update-View pattern to redux
- React Tea Cup: A fantastic port of The Elm Architecture to React. It resembles Elm closely, comes with a full system of effects and subscriptions and has great docs.
react-model-update-view
tries to be minimal and focuses on the basics. - Hyperapp is a tiny microframework that follows similar pattern