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
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"react/prefer-stateless-function": "off",
"react/destructuring-assignment": "off",
"react/state-in-constructor": "off",
"react/jsx-props-no-spreading": "off",
"react/prop-types": "off"
},
"globals": {
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,30 @@ This is not necessary if you use React Router 4.4+. You can find more details an
</details>
<p></p>

<details>
<summary>Usage with React Developer Tools.</summary>
<p></p>

If you want React Developer Tools to recognize your reactive view components' names, you have to pass either a **named function** or an anonymous function with **name inference** to the `view` wrapper.

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

const user = store({
name: 'Rick',
});

const componentName = () => (
<div>{user.name}</div>
);

export default view(componentName);
```

</details>
<p></p>

<details>
<summary>Passing nested data to third party components.</summary>
<p></p>
Expand Down
63 changes: 0 additions & 63 deletions __tests__/staticProps.test.js

This file was deleted.

115 changes: 115 additions & 0 deletions __tests__/staticProps.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* eslint-disable react/forbid-foreign-prop-types */
/* eslint-disable no-multi-assign */
import React, { Component } from 'react';
import { render, cleanup } from '@testing-library/react/pure';
// eslint-disable-next-line import/no-unresolved
import { view } from 'react-easy-state';
import PropTypes from 'prop-types';

describe('static props', () => {
afterEach(cleanup);

test('view() should proxy defaultProps for class components', () => {
class MyCustomCompName extends Component {
render() {
return <div>{this.props.name}</div>;
}
}

MyCustomCompName.defaultProps = {
name: 'Bob',
};

const WrappedComp = view(MyCustomCompName);
const { container } = render(<WrappedComp />);
expect(container).toHaveTextContent('Bob');
});

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

MyCustomCompName.defaultProps = {
name: 'Bob',
};

const WrappedComp = view(MyCustomCompName);
const { container } = render(<WrappedComp />);
expect(container).toHaveTextContent('Bob');
});

test('view() should proxy propTypes for class components', () => {
class MyCustomCompName extends Component {
render() {
return <div>{this.props.name}</div>;
}
}

MyCustomCompName.propTypes = {
name: PropTypes.string.isRequired,
};

const ViewComp = view(MyCustomCompName);

const errorSpy = jest
.spyOn(console, 'error')
.mockImplementation(message =>
expect(message.indexOf('Failed prop type')).not.toBe(-1),
);
render(<ViewComp number="Bob" />);
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});

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

MyCustomCompName.propTypes = {
number: PropTypes.number.isRequired,
};

const ViewComp = view(MyCustomCompName);

const errorSpy = jest
.spyOn(console, 'error')
.mockImplementation(message =>
expect(message.indexOf('Failed prop type')).not.toBe(-1),
);
render(<ViewComp number="Bob" />);
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});

test('view() should proxy static methods', () => {
class Comp extends Component {
static getDerivedStateFromError() {}

static customMethod() {}
}

const ViewComp = view(Comp);
expect(ViewComp.getDerivedStateFromError).toBe(
Comp.getDerivedStateFromError,
);
expect(ViewComp.customMethod).toBe(Comp.customMethod);
});

test('view() should proxy static getters', () => {
class Comp extends Component {
static get defaultProp() {
return { key: 'value' };
}

static get customProp() {
return { key: 'hello' };
}
}

const ViewComp = view(Comp);
expect(ViewComp.defaultProps).toEqual(Comp.defaultProps);
expect(ViewComp.customProp).toEqual(Comp.customProp);
});
});
6 changes: 3 additions & 3 deletions examples/beer-finder/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import NavBar from "./NavBar";
import BeerList from "./BeerList";
import React from 'react';
import NavBar from './NavBar';
import BeerList from './BeerList';

// if a component does not use any store, it doesn't have to be wrapped with view()
// it is safer to wrap everything with view() until you get more comfortable with Easy State
Expand Down
66 changes: 39 additions & 27 deletions examples/beer-finder/src/Beer.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import React from "react";
import { view, store } from "react-easy-state";
import Card from "@material-ui/core/Card";
import CardMedia from "@material-ui/core/CardMedia";
import CardContent from "@material-ui/core/CardContent";
import React from 'react';
import { view, store } from 'react-easy-state';
import Card from '@material-ui/core/Card';
import CardMedia from '@material-ui/core/CardMedia';
import CardContent from '@material-ui/core/CardContent';

// this is re-rendered whenever the relevant parts of the used data stores change
export default view(
({ name, description, image_url: imageUrl, food_pairing: foodPairing }) => {
const beer = store({ details: false });
const Beer = ({
name,
description,
image_url: imageUrl,
food_pairing: foodPairing,
}) => {
const beer = store({ details: false });

return (
<Card onClick={() => (beer.details = !beer.details)} className="beer">
{!beer.details && <CardMedia image={imageUrl} className="media" />}
<CardContent>
<h3>{name}</h3>
{beer.details ? (
<p>{description}</p>
) : (
<ul>
{foodPairing.map(food => (
<li key={food}>{food}</li>
))}
</ul>
)}
</CardContent>
</Card>
);
}
);
return (
<Card
onClick={() => {
beer.details = !beer.details;
}}
className="beer"
>
{!beer.details && (
<CardMedia image={imageUrl} className="media" />
)}
<CardContent>
<h3>{name}</h3>
{beer.details ? (
<p>{description}</p>
) : (
<ul>
{foodPairing.map(food => (
<li key={food}>{food}</li>
))}
</ul>
)}
</CardContent>
</Card>
);
};

export default view(Beer);
14 changes: 8 additions & 6 deletions examples/beer-finder/src/BeerList.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from "react";
import { view } from "react-easy-state";
import appStore from "./appStore";
import Beer from "./Beer";
import React from 'react';
import { view } from 'react-easy-state';
import appStore from './appStore';
import Beer from './Beer';

// this is re-rendered whenever the relevant parts of the used data stores change
export default view(() => (
const BeerList = () => (
<div className="beerlist">
{!appStore.beers.length ? (
<h3>No matching beers found!</h3>
) : (
appStore.beers.map(beer => <Beer key={beer.name} {...beer} />)
)}
</div>
));
);

export default view(BeerList);
16 changes: 9 additions & 7 deletions examples/beer-finder/src/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from "react";
import { view } from "react-easy-state";
import SearchBar from "material-ui-search-bar";
import LinearProgress from "@material-ui/core/LinearProgress";
import appStore from "./appStore";
import React from 'react';
import { view } from 'react-easy-state';
import SearchBar from 'material-ui-search-bar';
import LinearProgress from '@material-ui/core/LinearProgress';
import appStore from './appStore';

// this is re-rendered whenever the relevant parts of the used data stores change
export default view(() => (
const NavBar = () => (
<div className="searchbar">
<SearchBar
onRequestSearch={appStore.fetchBeers}
Expand All @@ -14,4 +14,6 @@ export default view(() => (
/>
{appStore.isLoading && <LinearProgress />}
</div>
));
);

export default view(NavBar);
8 changes: 5 additions & 3 deletions src/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function view(Comp) {

if (isStatelessComp && hasHooks) {
// use a hook based reactive wrapper when we can
ReactiveComp = memo(props => {
ReactiveComp = props => {
// use a dummy setState to update the component
const [, setState] = useState();
const triggerRender = useCallback(() => setState({}), []);
Expand Down Expand Up @@ -73,7 +73,7 @@ export function view(Comp) {
} finally {
isInsideFunctionComponent = false;
}
});
};
} else {
const BaseComp = isStatelessComp ? Component : Comp;
// a HOC which overwrites render, shouldComponentUpdate and componentWillUnmount
Expand Down Expand Up @@ -168,5 +168,7 @@ export function view(Comp) {
});
}

return ReactiveComp;
return isStatelessComp && hasHooks
? memo(ReactiveComp)
: ReactiveComp;
}