diff --git a/.eslintrc.json b/.eslintrc.json index b7597ab..cc9e7b1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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": { diff --git a/README.md b/README.md index 8e4f6df..ec7dbaa 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,30 @@ This is not necessary if you use React Router 4.4+. You can find more details an

+
+Usage with React Developer Tools. +

+ +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 = () => ( +
{user.name}
+); + +export default view(componentName); +``` + +
+

+
Passing nested data to third party components.

diff --git a/__tests__/staticProps.test.js b/__tests__/staticProps.test.js deleted file mode 100644 index ddd9151..0000000 --- a/__tests__/staticProps.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable react/forbid-foreign-prop-types */ -/* eslint-disable no-multi-assign */ -import { Component } from 'react'; -// eslint-disable-next-line import/no-unresolved -import { view } from 'react-easy-state'; - -describe('static props', () => { - test('view() should proxy static properties from wrapped components', () => { - class Comp extends Component {} - function FuncComp() {} - - Comp.displayName = FuncComp.displayName = 'Name'; - Comp.contextTypes = FuncComp.contextTypes = {}; - Comp.propTypes = FuncComp.propTypes = {}; - Comp.defaultProps = FuncComp.defaultProps = {}; - Comp.customProp = FuncComp.customProp = {}; - - const ViewComp = view(Comp); - const ViewFuncComp = view(FuncComp); - - expect(ViewComp.displayName).toBe(Comp.displayName); - expect(ViewComp.contextTypes).toBe(Comp.contextTypes); - expect(ViewComp.propTypes).toBe(Comp.propTypes); - expect(ViewComp.defaultProps).toBe(Comp.defaultProps); - expect(ViewComp.customProp).toBe(Comp.customProp); - - expect(ViewFuncComp.displayName).toBe(FuncComp.displayName); - expect(ViewFuncComp.contextTypes).toBe(FuncComp.contextTypes); - expect(ViewFuncComp.propTypes).toBe(FuncComp.propTypes); - expect(ViewFuncComp.defaultProps).toBe(FuncComp.defaultProps); - expect(ViewFuncComp.customProp).toBe(FuncComp.customProp); - }); - - 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); - }); -}); diff --git a/__tests__/staticProps.test.jsx b/__tests__/staticProps.test.jsx new file mode 100644 index 0000000..2902854 --- /dev/null +++ b/__tests__/staticProps.test.jsx @@ -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
{this.props.name}
; + } + } + + MyCustomCompName.defaultProps = { + name: 'Bob', + }; + + const WrappedComp = view(MyCustomCompName); + const { container } = render(); + expect(container).toHaveTextContent('Bob'); + }); + + test('view() should proxy defaultProps for functional components', () => { + const MyCustomCompName = props => { + return
{props.name}
; + }; + + MyCustomCompName.defaultProps = { + name: 'Bob', + }; + + const WrappedComp = view(MyCustomCompName); + const { container } = render(); + expect(container).toHaveTextContent('Bob'); + }); + + test('view() should proxy propTypes for class components', () => { + class MyCustomCompName extends Component { + render() { + return
{this.props.name}
; + } + } + + 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(); + expect(errorSpy).toHaveBeenCalled(); + errorSpy.mockRestore(); + }); + + test('view() should proxy propTypes for functional components', () => { + const MyCustomCompName = props => { + return
{props.number}
; + }; + + 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(); + 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); + }); +}); diff --git a/examples/beer-finder/src/App.jsx b/examples/beer-finder/src/App.jsx index a6beb0b..2b9bda4 100644 --- a/examples/beer-finder/src/App.jsx +++ b/examples/beer-finder/src/App.jsx @@ -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 diff --git a/examples/beer-finder/src/Beer.jsx b/examples/beer-finder/src/Beer.jsx index 43aa67a..67042e7 100644 --- a/examples/beer-finder/src/Beer.jsx +++ b/examples/beer-finder/src/Beer.jsx @@ -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 ( - (beer.details = !beer.details)} className="beer"> - {!beer.details && } - -

{name}

- {beer.details ? ( -

{description}

- ) : ( -
    - {foodPairing.map(food => ( -
  • {food}
  • - ))} -
- )} -
-
- ); - } -); + return ( + { + beer.details = !beer.details; + }} + className="beer" + > + {!beer.details && ( + + )} + +

{name}

+ {beer.details ? ( +

{description}

+ ) : ( +
    + {foodPairing.map(food => ( +
  • {food}
  • + ))} +
+ )} +
+
+ ); +}; + +export default view(Beer); diff --git a/examples/beer-finder/src/BeerList.jsx b/examples/beer-finder/src/BeerList.jsx index b542981..ed40330 100644 --- a/examples/beer-finder/src/BeerList.jsx +++ b/examples/beer-finder/src/BeerList.jsx @@ -1,10 +1,10 @@ -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 = () => (
{!appStore.beers.length ? (

No matching beers found!

@@ -12,4 +12,6 @@ export default view(() => ( appStore.beers.map(beer => ) )}
-)); +); + +export default view(BeerList); diff --git a/examples/beer-finder/src/NavBar.jsx b/examples/beer-finder/src/NavBar.jsx index 96c2a1f..cfff229 100644 --- a/examples/beer-finder/src/NavBar.jsx +++ b/examples/beer-finder/src/NavBar.jsx @@ -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 = () => (
( /> {appStore.isLoading && }
-)); +); + +export default view(NavBar); diff --git a/src/view.js b/src/view.js index 6052773..fa919b3 100644 --- a/src/view.js +++ b/src/view.js @@ -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({}), []); @@ -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 @@ -168,5 +168,7 @@ export function view(Comp) { }); } - return ReactiveComp; + return isStatelessComp && hasHooks + ? memo(ReactiveComp) + : ReactiveComp; }