Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"react/destructuring-assignment": "off",
"react/state-in-constructor": "off",
"react/jsx-props-no-spreading": "off",
"react/prop-types": "off"
"react/prop-types": "off",
"react/jsx-one-expression-per-line": "off"
},
"globals": {
"window": true,
Expand Down
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Simple React state management. Made with :heart: and ES6 Proxies.
+ [Creating global stores](#creating-global-stores)
+ [Creating reactive views](#creating-reactive-views)
+ [Creating local stores](#creating-local-stores)
+ [Adding side effects](#adding-side-effects)
* [Examples with live demos](#examples-with-live-demos)
* [Articles](#articles)
* [Performance](#performance)
Expand Down Expand Up @@ -479,7 +480,7 @@ export default view(() => {
**Local stores in functions rely on React hooks. They require React and React DOM v16.8+ or React Native v0.59+ to work.**

<details>
<summary>You can use any React hook - including <code>useState</code> - in function components, Easy State won't interfere with them.</summary>
<summary>You can use React hooks - including <code>useState</code> - in function components, Easy State won't interfere with them. Consider using <a href="#local-auto-effects-in-function-components">autoEffect</a> instead of the `useEffect` hook for the best experience though.</summary>
<p></p>

```jsx
Expand Down Expand Up @@ -623,6 +624,105 @@ Instead of returning an object, you should directly mutate the received stores.

</details>

### Adding side effects

Use `autoEffect` to react with automatic side effect to your store changes. Auto effects should contain end-of-chain logic - like changing the document title or saving data to LocalStorage. `view` is a special auto effect that does rendering.

<details>
<summary>Never use auto effects to derive data from other data. Use dynamic getters instead.</summary>
<p></p>

```jsx
import { store, autoEffect } from 'react-easy-state';

// DON'T DO THIS
const store1 = store({ name: 'Store 1' })
const store2 = store({ name: 'Store 2' })
autoEffect(() => store2.name = store1.name)

// DO THIS INSTEAD
const store1 = store({ name: 'Store 1' })
const store2 = store({ get name () { return store1.name } })
```

</details>
<p></p>

#### Global auto effects

Global auto effects can be created with `autoEffect` and cleared up with `clearEffect`.

```jsx
import { store, autoEffect, clearEffect } from 'react-easy-state';

const app = store({ name: 'My App' })
const effect = autoEffect(() => document.title = app.name)

// this also updates the document title
app.name = 'My Awesome App'

clearEffect(effect)
// this won't update the document title, the effect is cleared
app.name = 'My App'
```

#### Local auto effects in function components

Use local auto effects in function components instead of the `useEffect` hook when reactive stores are used inside them. These local effects are automatically cleared when the component unmounts.

```jsx
import React from 'react'
import { store, view, autoEffect } from 'react-easy-state';

export default view(() => {
const app = store({ name: 'My App' })
// no need to clear the effect
autoEffect(() => document.title = app.name)
})
```

<details>
<summary>Explicitly pass none reactive dependencies - like vanillas props and state - to local auto effects in function components.</summary>
<p></p>

Because of the design of React hooks you have to explicitly pass all none reactive data to a hook-like dependency array. This makes sure that the effect also runs when the none reactive data changes.

```jsx
import React from 'react'
import { store, view, autoEffect } from 'react-easy-state';

export default view(({ greeting }) => {
const app = store({ name: 'My App' })
// pass `greeting` in the dependency array because it is not coming from a store
autoEffect(() => document.title = `${greeting} ${app.name}`, [greeting])
})
```

</details>
<p></p>

#### Local auto effects in class components

Local effects in class components must be cleared when the component unmounts.

```jsx
import React, { Component } from 'react'
import { store, view, autoEffect } from 'react-easy-state';

class App extends Component {
app = store({ name: 'My App' })

componentDidMount () {
this.effect = autoEffect(() => document.title = this.app.name)
}

componentWillUnmount () {
// local effects in class components must be cleared on unmount
clearEffect(this.effect)
}
}
```

---

## Examples with live demos
Expand Down
48 changes: 48 additions & 0 deletions __tests__/autoEffect.no-hook.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { Component } from 'react';
import { render, cleanup } from '@testing-library/react/pure';
// eslint-disable-next-line import/no-unresolved
import { view, store, autoEffect } from 'react-easy-state';

describe('AutoEffect edge cases and errors', () => {
afterEach(cleanup);

test(`Using autoEffect in a function component ${
process.env.NOHOOK
? 'with a version of react that has no hooks should'
: 'should not'
} throw an error`, () => {
const someEffect = () => {};
const person = store({ name: 'Bob' });

const MyComp = view(() => {
autoEffect(() => someEffect(person.name));
return <div>{person.name}</div>;
});

if (process.env.NOHOOK) {
expect(() => render(<MyComp />)).toThrow(
'You cannot use autoEffect inside a function component with a pre-hooks version of React. Please update your React version to at least v16.8.0 to use this feature.',
);
} else {
expect(() => render(<MyComp />)).not.toThrow();
}
});

test('Using autoEffect inside a render of a class component should throw an error', () => {
const someEffect = () => {};
const person = store({ name: 'Bob' });

const MyComp = view(
class extends Component {
render() {
autoEffect(() => someEffect(person.name));
return <div>{person.name}</div>;
}
},
);

expect(() => render(<MyComp />)).toThrow(
'You cannot use autoEffect inside a render of a class component. Please use it in the constructor or lifecycle methods instead.',
);
});
});
94 changes: 94 additions & 0 deletions __tests__/autoEffect.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import { render, cleanup, act } from '@testing-library/react/pure';
import {
view,
store,
autoEffect,
clearEffect,
// eslint-disable-next-line import/no-unresolved
} from 'react-easy-state';

describe('autoEffect', () => {
afterEach(cleanup);

test('should auto run global effects', () => {
let documentTitle = '';
const app = store({ name: 'Online Store' });

const effect = autoEffect(() => {
documentTitle = app.name;
});
expect(documentTitle).toBe('Online Store');

act(() => {
app.name = 'Learning Platform';
});
expect(documentTitle).toBe('Learning Platform');

clearEffect(effect);
act(() => {
app.name = 'Social Platform';
});
expect(documentTitle).toBe('Learning Platform');
});

test('should auto run local effects in function components', () => {
let documentTitle = '';

const app = store({ name: 'Online Store' });

const MyComp = view(() => {
autoEffect(() => {
documentTitle = app.name;
});
return <div>{app.name}</div>;
});

const { container, unmount } = render(<MyComp />);
expect(container).toHaveTextContent('Online Store');
expect(documentTitle).toBe('Online Store');

act(() => {
app.name = 'Learning Platform';
});
expect(container).toHaveTextContent('Learning Platform');
expect(documentTitle).toBe('Learning Platform');

unmount();
act(() => {
app.name = 'Social Platform';
});
expect(documentTitle).toBe('Learning Platform');
});

test('should be recreated on dependency changes in function components', () => {
let documentTitle = '';

const app = store({ name: 'Store' });

const MyComp = view(({ name }) => {
autoEffect(() => {
documentTitle = `${name} ${app.name}`;
}, [name]);
return (
<div>
{name} {app.name}
</div>
);
});

const { container, rerender } = render(<MyComp name="Online" />);
expect(container).toHaveTextContent('Online Store');
expect(documentTitle).toBe('Online Store');

rerender(<MyComp name="Awesome" />);
expect(container).toHaveTextContent('Awesome Store');
expect(documentTitle).toBe('Awesome Store');

act(() => {
app.name = 'Page';
});
expect(container).toHaveTextContent('Awesome Page');
expect(documentTitle).toBe('Awesome Page');
});
});
2 changes: 1 addition & 1 deletion __tests__/store.no-hook.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, cleanup } from '@testing-library/react/pure';
// eslint-disable-next-line import/no-unresolved
import { view, store } from 'react-easy-state';

describe('Using an old react version', () => {
describe('Store edge cases and errors', () => {
afterEach(cleanup);

test(`Using local state in a function component ${
Expand Down
30 changes: 30 additions & 0 deletions src/autoEffect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { observe, unobserve } from '@nx-js/observer-util';

import {
isInsideFunctionComponent,
isInsideClassComponentRender,
isInsideFunctionComponentWithoutHooks,
} from './view';

export function autoEffect(fn, deps = []) {
if (isInsideFunctionComponent) {
return useEffect(() => {
const observer = observe(fn);
return () => unobserve(observer);
}, deps);
}
if (isInsideFunctionComponentWithoutHooks) {
throw new Error(
'You cannot use autoEffect inside a function component with a pre-hooks version of React. Please update your React version to at least v16.8.0 to use this feature.',
);
}
if (isInsideClassComponentRender) {
throw new Error(
'You cannot use autoEffect inside a render of a class component. Please use it in the constructor or lifecycle methods instead.',
);
}
return observe(fn);
}

export { unobserve as clearEffect };
3 changes: 1 addition & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { observe, unobserve } from '@nx-js/observer-util';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added since the last release and it is not yet released to npm. It won't cause a breaking change.


export { view } from './view';
export { store } from './store';
export { batch } from './scheduler';
export { autoEffect, clearEffect } from './autoEffect';