Skip to content
Merged
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
30 changes: 26 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
{
"parser": "babel-eslint",
"extends": ["airbnb", "prettier"],
"plugins": ["prettier"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"amd": true,
"node": true,
"es6": true
},
"extends": [
"airbnb",
"eslint:recommended",
"plugin:prettier/recommended"
],
"rules": {
"prettier/prettier": ["error"],
"prettier/prettier": ["error", {}, { "usePrettierrc": true }],
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": "off",
"import/no-mutable-exports": "off",
Expand All @@ -16,7 +37,8 @@
"react/state-in-constructor": "off",
"react/jsx-props-no-spreading": "off",
"react/prop-types": "off",
"react/jsx-one-expression-per-line": "off"
"react/jsx-one-expression-per-line": "off",
"react/jsx-closing-bracket-location": "off"
},
"globals": {
"window": true,
Expand Down
8 changes: 4 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["javascript"]
}
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Every pull request will require an approved review before merging.

## Linters

This project is using [ESLint](https://eslint.org/) for linting and [Prettier](https://prettier.io/) for code formatting. Please follow their standards on contributing. Your code is automatically formatted on saving a file when using [VSCode](https://code.visualstudio.com/) or [Atom](https://atom.io/). [Husky](https://github.com/typicode/husky) ensures that your changes are properly linted before making a commit. If you want to lint manually, you can use `lint` and `lint --fix` npm scripts to lint the source code and the test files.
This project is using [ESLint](https://eslint.org/) for linting and [Prettier](https://prettier.io/) for code formatting. Please follow their standards on contributing. Your code is automatically formatted on saving a file when using [VSCode](https://code.visualstudio.com/) or [Atom](https://atom.io/). [Husky](https://github.com/typicode/husky) ensures that your changes are properly linted before making a commit. If you want to lint manually, you can use `lint` and `lint-fix` npm scripts to lint the source code and the test files.

## Tests

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ person.name = 'Ann';
export default person;
```

The first example wouldn't trigger re-renders on the `person.name = 'Ann'` mutation, because it is targeted at the raw object. Mutating the raw - none `store`-wrapped object - won't schedule renders.
The first example wouldn't trigger re-renders on the `person.name = 'Ann'` mutation, because it is targeted at the raw object. Mutating the raw - non-`store`-wrapped object - won't schedule renders.

</details>
<p></p>
Expand Down Expand Up @@ -692,10 +692,10 @@ export default view(() => {
```

<details>
<summary>Explicitly pass none reactive dependencies - like vanillas props and state - to local auto effects in function components.</summary>
<summary>Explicitly pass non-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.
Because of the design of React hooks you have to explicitly pass all non-reactive data to a hook-like dependency array. This makes sure that the effect also runs when the non-reactive data changes.

```jsx
import React from 'react'
Expand Down
24 changes: 13 additions & 11 deletions __tests__/Clock.test.native.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import React, { StrictMode } from 'react';
import {
render,
flushMicrotasksQueue,
} from 'react-native-testing-library';

import renderer from 'react-test-renderer';

import sinon from 'sinon';
import App from '../examples/native-clock/App';

describe('Clock App', () => {
const clock = sinon.useFakeTimers();
const { getByText, unmount } = render(

const app = renderer.create(
<StrictMode>
<App />
</StrictMode>,
);
// flush the inital didMount effect
flushMicrotasksQueue();

const { unmount } = app;

const clearIntervalSpy = sinon.spy(global, 'clearInterval');

Expand All @@ -23,14 +23,16 @@ describe('Clock App', () => {
clearIntervalSpy.restore();
});

test('should update to display the current time every second', () => {
expect(getByText('12:00:00 AM')).toBeDefined();
test('should update to display the current time every second', async () => {
const textElement = app.root.findByType('Text');

expect(textElement.children[0]).toEqual('12:00:00 AM');

clock.tick(2000);
expect(getByText('12:00:02 AM')).toBeDefined();
expect(textElement.children[0]).toEqual('12:00:02 AM');

clock.tick(8500);
expect(getByText('12:00:10 AM')).toBeDefined();
expect(textElement.children[0]).toEqual('12:00:10 AM');
});

test('should clean up the interval timer when the component is unmounted', () => {
Expand Down
18 changes: 9 additions & 9 deletions __tests__/batching.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ describe('batching', () => {
expect(renderCount).toBe(1);
expect(container).toHaveTextContent('Bob');
await new Promise(
resolve =>
(resolve) =>
setTimeout(() => {
person.name = 'Ann';
person.name = 'Rick';
Expand All @@ -295,7 +295,7 @@ describe('batching', () => {
const { container } = render(<MyComp />);
expect(renderCount).toBe(1);
expect(container).toHaveTextContent('Bob');
await new Promise(resolve =>
await new Promise((resolve) =>
// eslint-disable-next-line
requestAnimationFrame(() => {
person.name = 'Ann';
Expand Down Expand Up @@ -418,18 +418,18 @@ describe('batching', () => {

test('should not break Promises', async () => {
await Promise.resolve(12)
.then(value => {
.then((value) => {
expect(value).toBe(12);
// eslint-disable-next-line
throw 15;
})
.catch(err => {
.catch((err) => {
expect(err).toBe(15);
});
});

test('should not break setTimeout', async () => {
await new Promise(resolve => {
await new Promise((resolve) => {
setTimeout(
(arg1, arg2, arg3) => {
expect(arg1).toBe('Hello');
Expand Down Expand Up @@ -459,10 +459,10 @@ describe('batching', () => {
expect(callCount).toBe(1);
});

test('should not break method this value and args', done => {
test('should not break method this value and args', (done) => {
const socket = new WebSocket('ws://www.example.com');

socket.onclose = function(ev) {
socket.onclose = function (ev) {
expect(ev).toBeDefined();
expect(this).toBe(socket);
done();
Expand All @@ -471,11 +471,11 @@ describe('batching', () => {
socket.close();
});

test('should not break callback this value and args', done => {
test('should not break callback this value and args', (done) => {
const ctx = {};

setTimeout(
function(arg1, arg2) {
function (arg1, arg2) {
expect(arg1).toBe('Test');
expect(arg2).toBe('Test2');
expect(this).toBe(ctx);
Expand Down
62 changes: 59 additions & 3 deletions __tests__/edgeCases.test.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import React, { Component, useState } from 'react';
import React, {
Component,
useState,
StrictMode,
useEffect,
useRef,
} from 'react';
import {
render,
cleanup,
fireEvent,
act,
} from '@testing-library/react/pure';
// eslint-disable-next-line import/no-unresolved
import { view, store, batch } from '@risingstack/react-easy-state';
Expand Down Expand Up @@ -57,7 +64,7 @@ describe('edge cases', () => {
state = { counter: 0 };

handleIncrement = () => {
this.setState(prevState => ({
this.setState((prevState) => ({
counter: prevState.counter + 1,
}));
};
Expand All @@ -84,7 +91,7 @@ describe('edge cases', () => {
state = { counter: 0 };

handleIncrement = () => {
this.setState(prevState => ({
this.setState((prevState) => ({
counter: prevState.counter + 1,
}));
};
Expand All @@ -111,6 +118,55 @@ describe('edge cases', () => {
expect(RawChild.mock.calls.length).toBe(1);
});

test('should not perform an update on an unmounted component (Strict Mode)', () => {
const person = store({ name: 'Bob' });

function MyComp() {
const [showChild, setChild] = useState(true);
return (
<StrictMode>
<div>
<button
onClick={() => setChild((value) => !value)}
type="button"
>
Toggle Child
</button>
{showChild && <Child />}
</div>
</StrictMode>
);
}

const RawChild = jest.fn().mockImplementation(function Child() {
const isMouted = useRef(false);
useEffect(() => {
isMouted.current = true;
}, []);
return <p>{person.name}</p>;
});
const Child = view(RawChild);

jest.spyOn(global.console, 'error');

const { container } = render(<MyComp />);

// Hide the Child component.
act(() => {
fireEvent.click(container.querySelector('button'));
});
// Show the Child component again.
act(() => {
fireEvent.click(container.querySelector('button'));
});
// Trigger Child update.
act(() => {
person.name = 'Ann';
});

expect(global.console.error.mock.calls.length).toBe(0);
});

test('view() should respect custom deriveStoresFromProps', () => {
const MyComp = view(
class extends Component {
Expand Down
27 changes: 18 additions & 9 deletions __tests__/staticProps.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('static props', () => {
});

test('view() should proxy defaultProps for functional components', () => {
const MyCustomCompName = props => {
const MyCustomCompName = (props) => {
return <div>{props.name}</div>;
};

Expand All @@ -50,20 +50,25 @@ describe('static props', () => {
name: PropTypes.string.isRequired,
};

const ViewComp = view(MyCustomCompName);
const ViewComp = MyCustomCompName;

const errorSpy = jest
.spyOn(console, 'error')
.mockImplementation(message =>
expect(message.indexOf('Failed prop type')).not.toBe(-1),
);
.mockImplementation((warning, prop, error) => {
expect(warning).toBe('Warning: Failed %s type: %s%s');
expect(prop).toBe('prop');
expect(error).toBe(
'The prop `name` is marked as required in `MyCustomCompName`, but its value is `undefined`.',
);
});
expect(1).toBe(1);
render(<ViewComp number="Bob" />);
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});

test('view() should proxy propTypes for functional components', () => {
const MyCustomCompName = props => {
const MyCustomCompName = (props) => {
return <div>{props.number}</div>;
};

Expand All @@ -75,9 +80,13 @@ describe('static props', () => {

const errorSpy = jest
.spyOn(console, 'error')
.mockImplementation(message =>
expect(message.indexOf('Failed prop type')).not.toBe(-1),
);
.mockImplementation((warning, prop, error) => {
expect(warning).toBe('Warning: Failed %s type: %s%s');
expect(prop).toBe('prop');
expect(error).toBe(
'Invalid prop `number` of type `string` supplied to `MyCustomCompName`, expected `number`.',
);
});
render(<ViewComp number="Bob" />);
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
Expand Down
Loading