From cd8777a7aefa7678ed52682d464ea2972ce17434 Mon Sep 17 00:00:00 2001 From: wallzero Date: Wed, 4 Sep 2019 17:35:20 -0400 Subject: [PATCH] feat(package): fixes to support React.StrictMode (#512) --- examples/typescript/index.tsx | 26 +++---- examples/typescript/package.json | 2 +- package.json | 3 +- src/components/UIRouter.tsx | 67 ++++++++++--------- src/components/UISref.tsx | 6 +- src/components/UISrefActive.tsx | 10 +-- src/components/UIView.tsx | 26 +++++-- src/components/__tests__/UISref.test.tsx | 4 +- .../__tests__/UISrefActive.test.tsx | 5 ++ src/components/__tests__/UIView.test.tsx | 24 ++++--- src/core.ts | 4 +- src/interface.tsx | 4 +- tsconfig.jest.json | 2 +- 13 files changed, 108 insertions(+), 75 deletions(-) diff --git a/examples/typescript/index.tsx b/examples/typescript/index.tsx index a41b0560..cdf131a3 100755 --- a/examples/typescript/index.tsx +++ b/examples/typescript/index.tsx @@ -55,17 +55,19 @@ const routerConfig = (router: UIRouterReact) => { let el = document.getElementById('react-app'); let app = ( - -
- - - Home - - - }> -

Content will load here

-
-
-
+ + +
+ + + Home + + + }> +

Content will load here

+
+
+
+
); ReactDOM.render(app, el); diff --git a/examples/typescript/package.json b/examples/typescript/package.json index b907e96b..c096466b 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -17,6 +17,6 @@ "tsconfig-paths-webpack-plugin": "^3.0.4", "typescript": "2.8.3", "webpack": "^4.7.0", - "webpack-dev-server": "^3.1.4" + "webpack-dev-server": "3.7.1" } } diff --git a/package.json b/package.json index faf2fad7..c711029b 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "react": "^16.5.1", "react-dom": "^16.5.1", "react-test-renderer": "^16.5.1", + "tsconfig-paths-webpack-plugin": "^3.2.0", "ts-jest": "^23.1.3", "ts-loader": "^6.0.2", "typescript": "^3.0.1", @@ -96,7 +97,7 @@ "testRegex": "/__tests__/.*\\.(ts|tsx|js)$", "globals": { "ts-jest": { - "tsConfigFile": "../tsconfig.jest.json" + "tsConfigFile": "./tsconfig.jest.json" } } }, diff --git a/src/components/UIRouter.tsx b/src/components/UIRouter.tsx index 98125cbe..a83f92c1 100644 --- a/src/components/UIRouter.tsx +++ b/src/components/UIRouter.tsx @@ -3,10 +3,10 @@ * @module components */ /** */ import * as React from 'react'; -import { Component, Children } from 'react'; +import { Component } from 'react'; import * as PropTypes from 'prop-types'; -import { UIRouterPlugin, servicesPlugin } from '@uirouter/core'; +import { servicesPlugin } from '@uirouter/core'; import { UIRouterReact, ReactStateDeclaration } from '../index'; @@ -44,21 +44,21 @@ export interface UIRouterState { /** @hidden */ export const InstanceOrPluginsMissingError = new Error(`Router instance or plugins missing. -You must either provide a location plugin via the plugins prop: - - - - - -or initialize the router yourself and pass the instance via props: - -const router = new UIRouterReact(); -router.plugin(pushStateLocationPlugin); -··· - - - -`); + You must either provide a location plugin via the plugins prop: + + + + + + or initialize the router yourself and pass the instance via props: + + const router = new UIRouterReact(); + router.plugin(pushStateLocationPlugin); + ··· + + + + `); /** @hidden */ export const UIRouterInstanceUndefinedError = new Error( @@ -76,24 +76,27 @@ export class UIRouter extends Component { router: UIRouterReact; - constructor(props, context) { - super(props, context); - // check if a router instance is provided - if (props.router) { - this.router = props.router; - } else if (props.plugins) { - this.router = new UIRouterReact(); - this.router.plugin(servicesPlugin); - props.plugins.forEach(plugin => this.router.plugin(plugin)); - if (props.config) props.config(this.router); - (props.states || []).forEach(state => this.router.stateRegistry.register(state)); - } else { - throw InstanceOrPluginsMissingError; + componentDidMount() { + if (!this.router) { + // check if a router instance is provided + if (this.props.router) { + this.router = this.props.router; + } else if (this.props.plugins) { + this.router = new UIRouterReact(); + this.router.plugin(servicesPlugin); + this.props.plugins.forEach(plugin => this.router.plugin(plugin)); + if (this.props.config) this.props.config(this.router); + (this.props.states || []).forEach(state => this.router.stateRegistry.register(state)); + } else { + throw InstanceOrPluginsMissingError; + } + + this.router.start(); + this.forceUpdate(); } - this.router.start(); } render() { - return {this.props.children}; + return this.router ? {this.props.children} : null; } } diff --git a/src/components/UISref.tsx b/src/components/UISref.tsx index 213a5e30..09a5ef3d 100644 --- a/src/components/UISref.tsx +++ b/src/components/UISref.tsx @@ -44,7 +44,7 @@ class Sref extends Component { className: PropTypes.string, }; - componentWillMount() { + componentDidMount() { const addStateInfo = this.props.addStateInfoToParentActive; this.deregister = typeof addStateInfo === 'function' ? addStateInfo(this.props.to, this.props.params) : () => {}; const router = this.props.router; @@ -54,7 +54,9 @@ class Sref extends Component { } componentWillUnmount() { - this.deregister(); + if (this.deregister) { + this.deregister(); + } } getOptions = () => { diff --git a/src/components/UISrefActive.tsx b/src/components/UISrefActive.tsx index 94a9d08c..22b078ec 100644 --- a/src/components/UISrefActive.tsx +++ b/src/components/UISrefActive.tsx @@ -3,11 +3,11 @@ * @module components */ /** */ import * as React from 'react'; -import { Component, cloneElement, ValidationMap } from 'react'; +import { Component, cloneElement } from 'react'; import * as PropTypes from 'prop-types'; import * as _classNames from 'classnames'; -import { UIRouterReact, UISref, UIRouterConsumer } from '../index'; +import { UIRouterReact, UIRouterConsumer } from '../index'; import { UIViewAddress } from './UIView'; import { UIRouterInstanceUndefinedError } from './UIRouter'; @@ -59,7 +59,7 @@ class SrefActive extends Component { activeClasses: '', }; - componentWillMount() { + componentDidMount() { const router = this.props.router; if (typeof router === 'undefined') { throw UIRouterInstanceUndefinedError; @@ -69,7 +69,9 @@ class SrefActive extends Component { } componentWillUnmount() { - this.deregister(); + if (this.deregister) { + this.deregister(); + } } addStateInfo = (stateName, stateParams) => { diff --git a/src/components/UIView.tsx b/src/components/UIView.tsx index afb80cfa..eaddbd1d 100644 --- a/src/components/UIView.tsx +++ b/src/components/UIView.tsx @@ -8,13 +8,15 @@ import { ClassicComponentClass, Component, ComponentClass, + createContext, SFC, + ReactNode, StatelessComponent, ValidationMap, Validator, cloneElement, createElement, - isValidElement, + isValidElement } from 'react'; import * as PropTypes from 'prop-types'; @@ -78,6 +80,7 @@ export interface UIViewInjectedProps { /** Component Props for `UIView` */ export interface UIViewProps { + children?: ReactNode; router?: UIRouterReact; parentUIView?: UIViewAddress; name?: string; @@ -100,7 +103,7 @@ export const TransitionPropCollisionError = new Error( ); /** @internalapi */ -export const { Provider: UIViewProvider, Consumer: UIViewConsumer } = React.createContext(undefined); +export const { Provider: UIViewProvider, Consumer: UIViewConsumer } = createContext(undefined); class View extends Component { // This object contains all the metadata for this UIView @@ -168,7 +171,7 @@ class View extends Component { return {ChildOrRenderFunction}; } - componentWillMount() { + componentDidMount() { const router = this.props.router; if (typeof router === 'undefined') { throw UIRouterInstanceUndefinedError; @@ -199,7 +202,9 @@ class View extends Component { } componentWillUnmount() { - this.deregister(); + if (this.deregister) { + this.deregister(); + } } /** @@ -262,9 +267,18 @@ class View extends Component { } } -export class UIView extends React.Component { +View.propTypes = { + router: PropTypes.object.isRequired as Validator, + parentUIView: PropTypes.object as Validator, + name: PropTypes.string, + className: PropTypes.string, + style: PropTypes.object, + render: PropTypes.func, +} as ValidationMap; + +export class UIView extends Component { static displayName = 'UIView'; - static __internalViewComponent: React.ComponentClass = View; + static __internalViewComponent: ComponentClass = View; render() { return ( diff --git a/src/components/__tests__/UISref.test.tsx b/src/components/__tests__/UISref.test.tsx index 5ba4a21d..b7235f59 100644 --- a/src/components/__tests__/UISref.test.tsx +++ b/src/components/__tests__/UISref.test.tsx @@ -139,7 +139,8 @@ describe('', () => { ); await router.stateService.go('state'); wrapper.update(); - const stateServiceGoSpy = jest.spyOn(wrapper.instance().router.stateService, 'go'); + // @ts-ignore + const stateServiceGoSpy = jest.spyOn(router.stateService, 'go'); const link = wrapper.find('a'); link.simulate('click'); link.simulate('click', { button: 1 }); @@ -162,6 +163,7 @@ describe('', () => { .find('Sref') .at(0); expect(uiSref.instance().context.parentUIViewAddress).toBeUndefined(); + // @ts-ignore expect(uiSref.instance().getOptions().relative.name).toBe(''); }); }); diff --git a/src/components/__tests__/UISrefActive.test.tsx b/src/components/__tests__/UISrefActive.test.tsx index b3323127..e09e081c 100644 --- a/src/components/__tests__/UISrefActive.test.tsx +++ b/src/components/__tests__/UISrefActive.test.tsx @@ -204,6 +204,7 @@ describe('', () => { .at(0) .instance(); expect(instance.context.parentUIViewAddress).toBeUndefined(); + // @ts-ignore expect(instance.states[0].state.name).toBe('parent.child1'); }); @@ -232,6 +233,7 @@ describe('', () => { .at(0) .instance(); expect(instance.context.parentUIViewAddress).toBeUndefined(); + // @ts-ignore expect(instance.states.length).toBe(3); }); @@ -266,6 +268,7 @@ describe('', () => { .find('SrefActive') .at(0) .instance(); + // @ts-ignore expect(instance.states.length).toBe(3); router.stateRegistry.register({ @@ -340,8 +343,10 @@ describe('', () => { .find('SrefActive') .at(0) .instance(); + // @ts-ignore expect(instance.states.length).toBe(1); wrapper.setProps({ show: false }); + // @ts-ignore expect(instance.states.length).toBe(0); }); diff --git a/src/components/__tests__/UIView.test.tsx b/src/components/__tests__/UIView.test.tsx index 3c0e030a..1e150654 100644 --- a/src/components/__tests__/UIView.test.tsx +++ b/src/components/__tests__/UIView.test.tsx @@ -106,15 +106,14 @@ describe('', () => { states.forEach(state => router.stateRegistry.register(state as ReactStateDeclaration)); }); - it('renders its State Component', () => { + it('renders its State Component', async () => { const wrapper = mount( ); - return router.stateService.go('parent').then(() => { - expect(wrapper.html()).toEqual(`
parent
`); - }); + await router.stateService.go('parent'); + expect(wrapper.update().html()).toEqual(`
parent
`); }); it('injects the right props', async () => { @@ -131,7 +130,9 @@ describe('', () => { ); await router.stateService.go('__state'); wrapper.update(); + // @ts-ignore expect(wrapper.find(Comp).props().myresolve).not.toBeUndefined(); + // @ts-ignore expect(wrapper.find(Comp).props().transition).not.toBeUndefined(); }); @@ -149,6 +150,7 @@ describe('', () => { ); await router.stateService.go('__state'); wrapper.update(); + // @ts-ignore expect(wrapper.find(Comp).props().foo).toBe('bar'); }); @@ -208,9 +210,9 @@ describe('', () => { ); await router.stateService.go('parent.child'); - expect(wrapper.html()).toEqual(`
parentchild
`); + expect(wrapper.update().html()).toEqual(`
parentchild
`); await router.stateService.go('parent'); - expect(wrapper.html()).toEqual(`
parent
`); + expect(wrapper.update().html()).toEqual(`
parent
`); }); it('calls uiCanExit function of its State Component when unmounting', async () => { @@ -239,9 +241,9 @@ describe('', () => { ); await router.stateService.go('__state'); - expect(wrapper.html()).toEqual('UiCanExitHookComponent'); + expect(wrapper.update().html()).toEqual('UiCanExitHookComponent'); await router.stateService.go('exit'); - expect(wrapper.html()).toEqual('exit'); + expect(wrapper.update().html()).toEqual('exit'); expect(uiCanExitSpy).toBeCalled(); }); @@ -273,9 +275,9 @@ describe('', () => { ); await router.stateService.go('__state'); console.log(wrapper.html()); - expect(wrapper.html()).toEqual('UiCanExitHookComponent'); + expect(wrapper.update().html()).toEqual('UiCanExitHookComponent'); await router.stateService.go('exit'); - expect(wrapper.html()).toEqual('exit'); + expect(wrapper.update().html()).toEqual('exit'); expect(uiCanExitSpy).toBeCalled(); }); @@ -300,7 +302,7 @@ describe('', () => { ); await router.stateService.go('withrenderprop'); - expect(wrapper.html()).toEqual(`
withrenderpropbar
`); + expect(wrapper.update().html()).toEqual(`
withrenderpropbar
`); }); it('unmounts the State Component when calling stateService.reload(true)', async () => { diff --git a/src/core.ts b/src/core.ts index 76d4bc0c..05e4b13d 100644 --- a/src/core.ts +++ b/src/core.ts @@ -2,8 +2,8 @@ * @reactapi * @module react */ /** */ -import { UIRouter, PathNode, services } from '@uirouter/core'; -import { ReactViewDeclaration, ReactStateDeclaration } from './interface'; +import { UIRouter, PathNode } from '@uirouter/core'; +import { ReactViewDeclaration } from './interface'; import { ReactViewConfig, reactViewsBuilder } from './reactViews'; /** diff --git a/src/interface.tsx b/src/interface.tsx index 46a816e0..2a24b784 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -2,9 +2,9 @@ * @reactapi * @module react */ /** */ -import { Component, ReactElement, StatelessComponent, ComponentClass, ClassicComponentClass } from 'react'; +import { StatelessComponent, ComponentClass, ClassicComponentClass } from 'react'; -import { StateDeclaration, _ViewDeclaration, ParamDeclaration, IInjectable, Transition } from '@uirouter/core'; +import { StateDeclaration, _ViewDeclaration, Transition } from '@uirouter/core'; /** * The StateDeclaration object is used to define a state or nested state. diff --git a/tsconfig.jest.json b/tsconfig.jest.json index 02328abc..75a19593 100644 --- a/tsconfig.jest.json +++ b/tsconfig.jest.json @@ -1,5 +1,5 @@ { - "compilerOption": { + "compilerOptions": { "jsx": "react", "module": "commonjs", "target": "es6"