mirror of
https://github.com/mmalmi/mmalmi.github.io.git
synced 2025-05-22 01:52:04 +00:00
113 lines
3.5 KiB
Markdown
113 lines
3.5 KiB
Markdown
---
|
||
layout: post
|
||
title: "Gun.js: Painless React State Management"
|
||
date: 2021-9-27 01:37:00 +0000
|
||
tags: software
|
||
published: true
|
||
description: "React state synchronization and persistence in just a few lines of code."
|
||
image: https://siriusbusiness.fi/assets/images/posts/painless.jpg
|
||
---
|
||
|
||

|
||
|
||
Fed up with writing a ton of Redux boilerplate just to handle form input changes?
|
||
|
||
There’s a better alternative: [Gun.js](https://github.com/amark/gun). It makes state synchronization and persistence a breeze.
|
||
|
||
First, initialize Gun with options that ensure the state is synced with localStorage only (not with peers).
|
||
```jsx
|
||
const State = new Gun({
|
||
localStorage: true,
|
||
file: 'State.local', // localStorage key
|
||
multicast: false, // on Node.js, Gun would sync with local area network peers over multicast :)
|
||
peers: [] // this time we don't want to sync with other users
|
||
}).get('state');
|
||
```
|
||
|
||
Then create a React component that uses the state:
|
||
|
||
```jsx
|
||
class TextInput extends React.Component {
|
||
state = {text:''};
|
||
|
||
componentDidMount() {
|
||
State.get('text').on(text => this.setState({text}));
|
||
}
|
||
|
||
onInput(e) {
|
||
State.get('text').put(e.target.value);
|
||
}
|
||
|
||
render() {
|
||
return (
|
||
<form class="box">
|
||
<input placeholder="Type something"
|
||
type="text"
|
||
value={this.state.text}
|
||
onChange={e => this.onInput(e)} />
|
||
</form>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
Now you have a text input that syncs its content across component instances and persists it in localStorage.
|
||
|
||
See the working example on [Codepen](https://codepen.io/mmalmi/pen/VwWVdKG).
|
||
|
||
However, if we now unmount the component and write to `State.get('text')` elsewhere, we'll get the warning: `Can't call setState on an unmounted component. This is a no-op, but it indicates a memory leak in your application.`
|
||
|
||
We can avoid that by saving our state subscriptions and unsubscribing them in `componentWillUnmount()`. Here's a helper class for that:
|
||
|
||
```jsx
|
||
class BaseComponent extends React.Component {
|
||
subscriptions = {};
|
||
|
||
subscribe(callback, path) {
|
||
return (value, key, x, subscription, f) => {
|
||
if (this.unmounted) {
|
||
subscription && subscription.off();
|
||
return;
|
||
}
|
||
this.subscriptions[path || key] = subscription;
|
||
callback(value, key, x, subscription, f);
|
||
}
|
||
}
|
||
|
||
inject(name, path) {
|
||
return this.subscribe((v,k) => {
|
||
const newState = {};
|
||
newState[name || k] = v;
|
||
this.setState(newState);
|
||
}, path);
|
||
}
|
||
|
||
componentWillUnmount() {
|
||
this.unmounted = true;
|
||
Object.keys(this.subscriptions).forEach(k => {
|
||
const subscription = this.subscriptions[k];
|
||
subscription && subscription.off();
|
||
delete this.subscriptions[k];
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
Now we can extend BaseComponent, which takes care of injecting values to the component's state and unsubscribing on unmount.
|
||
|
||
```jsx
|
||
class TextInput extends BaseComponent {
|
||
// ...
|
||
componentDidMount() {
|
||
State.get('text').on(this.inject());
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
[Codepen](https://codepen.io/mmalmi/pen/MWozPZE)
|
||
|
||
Prefer *React hooks*? Check out this [codepen](https://codepen.io/alterx/pen/VwWqgRV) by [Carlos](https://codepen.io/alterx).
|
||
|
||
Upcoming: How to sync your application state with the world.
|