diff --git a/README.md b/README.md index 8bf134d3..408c01b8 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,6 @@ The rendering in the function will be tracked and automatically re-rendered when This can come in handy when needing to pass render function to external components (for example the React Native listview), or if you dislike the `observer` decorator / function. -Example: - ```javascript class App extends React.Component { render() { @@ -105,6 +103,53 @@ React.render(, document.body) person.name = "Mike" // will cause the Observer region to re-render ``` +In case you are a fan of render props, you can use that instead of children. Be advised, that you cannot use both approaches at once, children have a precedence. +Example + +```javascript +class App extends React.Component { + render() { + return ( +
+ {this.props.person.name} +
{this.props.person.name}
} + /> +
+ ) + } +} + +const person = observable({ name: "John" }) + +React.render(, document.body) +person.name = "Mike" // will cause the Observer region to re-render +``` +Observer can also inject the stores simply by passing a selector function. +Example with inject + +```javascript + +const NameDisplayer = ({ name }) =>

{name}

+ +const user = mobx.observable({ + name: "Noa" +}) + +const UserNameDisplayer = ()=>( + ({user:stores.user})} + render={props => ()} + /> +) + +const App = () => ( + + + +) +``` + ### Global error handler with `onError` If a component throws an error, this logs to the console but does not 'crash' the app, so it might go unnoticed. @@ -438,3 +483,5 @@ Data will have one of the following formats: WeakMap. Its `get` function returns the associated reactive component of the given node. The node needs to be precisely the root node of the component. This map is only available after invoking `trackComponents`. + + diff --git a/src/index.d.ts b/src/index.d.ts index b687cc3e..69ff1f16 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -65,7 +65,7 @@ export function onError(cb: (error: Error) => void): () => void export class Provider extends React.Component {} -export class Observer extends React.Component<{ children?: () => React.ReactNode }, {}> {} +export class Observer extends React.Component<{ children?: () => React.ReactNode, render?: () => React.ReactNode, inject?: IStoresToProps | string[] }, {}> {} export function useStaticRendering(value: boolean): void diff --git a/src/observer.js b/src/observer.js index ac44c7b9..de1517bc 100644 --- a/src/observer.js +++ b/src/observer.js @@ -1,5 +1,5 @@ import { Atom, Reaction, extras } from "mobx" -import { Component } from "react" +import React, { Component } from "react" import { findDOMNode as baseFindDOMNode } from "react-dom" import EventEmitter from "./utils/EventEmitter" import inject from "./inject" @@ -349,22 +349,42 @@ function mixinLifecycleEvents(target) { } // TODO: support injection somehow as well? -export const Observer = observer(({ children }) => children()) +export const Observer = observer(({ children, inject: observerInject, render }) => { + const component = children || render + if (typeof component === "undefined") { + return null + } + if (!observerInject) { + return component() + } + const InjectComponent = inject(observerInject)(component) + return +}) Observer.displayName = "Observer" -Observer.propTypes = { - children: (propValue, key, componentName, location, propFullName) => { - if (typeof propValue[key] !== "function") - return new Error( - "Invalid prop `" + - propFullName + - "` of type `" + - typeof propValue[key] + - "` supplied to" + - " `" + - componentName + - "`, expected `function`." - ) +const ObserverPropsCheck = (props,key,componentName,location,propFullName)=>{ + const extraKey = key === "children" ? "render" : "children" + if(typeof propValue[key] === "function" && typeof propValue[extraKey] === "function" ){ + return new Error("Invalid prop,do not use children and render in the same time in`" + componentName ) } + + if (typeof propValue[key] === "function" || typeof propValue[extraKey] === "function") { + return + } + return new Error( + "Invalid prop `" + + propFullName + + "` of type `" + + typeof propValue[key] + + "` supplied to" + + " `" + + componentName + + "`, expected `function`." + ) +} + +Observer.propTypes = { + render: ObserverPropsCheck, + children: ObserverPropsCheck, } diff --git a/test/observer.test.js b/test/observer.test.js index 3b47b733..ecde7ed4 100644 --- a/test/observer.test.js +++ b/test/observer.test.js @@ -4,8 +4,8 @@ import ReactDOM from "react-dom" import ReactDOMServer from "react-dom/server" import TestUtils from "react-dom/test-utils" import * as mobx from "mobx" -import { observer, inject, onError, offError, useStaticRendering, Observer } from "../" -import { createTestRoot, sleepHelper, asyncReactDOMRender } from "./index" +import { observer, inject, onError, offError, useStaticRendering, Observer, Provider } from "../" +import { createTestRoot, sleepHelper, asyncReactDOMRender, asyncRender } from "./" import ErrorCatcher from "./ErrorCatcher" /** @@ -16,12 +16,6 @@ const testRoot = createTestRoot() const getDNode = (obj, prop) => obj.$mobx.values[prop] -const asyncRender = (element, root) => { - return new Promise(resolve => { - ReactDOM.render() - }) -} - /* use TestUtils.renderIntoDocument will re-mounted the component with with different props some misunderstanding will be cause? @@ -209,7 +203,6 @@ describe("does not views alive when using static rendering", () => { }) test("no re-rendering on static rendering", () => { - expect(renderCount).toBe(1) data.z = "hello" expect(renderCount).toBe(1) expect(TestUtils.findRenderedDOMComponentWithTag(element, "div").innerHTML).toBe("hi") @@ -533,9 +526,10 @@ describe("it rerenders correctly if some props are non-observables - 2", () => { } mobx.reaction(() => odata.x, v => console.log(v)) - - beforeAll(async () => { + + beforeAll(async done => { await asyncReactDOMRender(, testRoot) + done() }) test("init renderCount === 1", () => { @@ -757,3 +751,60 @@ test.skip("195 - should throw if trying to overwrite lifecycle methods", () => { /Cannot assign to read only property 'componentWillMount'/ ) }) + +describe("use Observer inject and render sugar should work ", () => { + test("use render without inject should be correct", async () => { + const Comp = () => ( +
+ {123}} /> +
+ ) + await asyncReactDOMRender(, testRoot) + expect(testRoot.querySelector("span").innerHTML).toBe("123") + }) + + test("use children without inject should be correct", async () => { + const Comp = () => ( +
+ {props => {123}} +
+ ) + await asyncReactDOMRender(, testRoot) + expect(testRoot.querySelector("span").innerHTML).toBe("123") + }) + + test("use render with inject should be correct", async () => { + const Comp = () => ( +
+ ({ h: store.h, w: store.w })} + render={props => {`${props.h} ${props.w}`}} + /> +
+ ) + const A = () => ( + + + + ) + await asyncReactDOMRender(, testRoot) + expect(testRoot.querySelector("span").innerHTML).toBe("hello world") + }) + + test("use children with inject should be correct", async () => { + const Comp = () => ( +
+ ({ h: store.h, w: store.w })}> + {props => {`${props.h} ${props.w}`}} + +
+ ) + const A = () => ( + + + + ) + await asyncReactDOMRender(
, testRoot) + expect(testRoot.querySelector("span").innerHTML).toBe("hello world") + }) +}) diff --git a/test/ts/compile-ts.tsx b/test/ts/compile-ts.tsx index 83994866..4c1aa0ce 100644 --- a/test/ts/compile-ts.tsx +++ b/test/ts/compile-ts.tsx @@ -191,6 +191,18 @@ class ObserverTest extends Component { } } +class ObserverTest2 extends Component { + render() { + return
test
} />; + } +} + +class ObserverTest3 extends Component { + render() { + return
test
} />; + } +} + @observer class ComponentWithoutPropsAndState extends Component<{}, {}> { componentDidUpdate() {