From a77f94d57fc42403d0942d26b10f4110dfe18df3 Mon Sep 17 00:00:00 2001 From: Alexandre GOMES Date: Wed, 15 Aug 2018 21:58:40 +0200 Subject: [PATCH] (split)[react-jss] Add flow typings (#818) * First commit * Fixes on createHOC and injectSheet * More fixes on createHOC and injectSheet * Better flow typings on createHOC * Moved types/index.js just to types.js and added InnerProps and OuterProps utility types * Simplified typings of injectSheet.js * Updated and fixed some typings in createHOC.js - Used OuterProps and InnerProps types - Fixed the props of the returning component * Removed unnecessary types in ns.js * Added flow flag to some untyped files * Exporting StyleSheetFactoryOptions now instead of StyleSheetOptions * Fix typing of getDisplayName * Added Context Type and updated Options to use StyleSheetFactoryOptions type * Moved types/index.js just to types.js and added * Improved Context Type * Updated createHoc to use Context type * Improved typing of the Theming option * Formatted code with prettier * Updated size snapshot and lockfile * Make options in injectSheet optional an default it to an empty object * Fixed types of the JssProvider props - Made most props optional - Only allow one child * Remove semi eslint rule * Removed unnecessary types * Remove unnecessary check * Updated a if statement and fixed removing dynamic sheet from global registry after component unmounts * Fixed naming of some type names to always start with an uppercase char * Renamed generic types of createHOC --- .size-snapshot.json | 28 ++++++++--------- package-lock.json | 16 +++++----- src/JssProvider.js | 26 +++++++++++++--- src/compose.js | 4 ++- src/contextTypes.js | 1 + src/createHoc.js | 71 +++++++++++++++++++++++++++++++------------ src/getDisplayName.js | 6 +++- src/index.js | 1 + src/injectSheet.js | 11 +++++-- src/jss.js | 2 ++ src/ns.js | 1 + src/propTypes.js | 1 + src/types.js | 43 ++++++++++++++++++++++++++ 13 files changed, 161 insertions(+), 50 deletions(-) create mode 100644 src/types.js diff --git a/.size-snapshot.json b/.size-snapshot.json index f05262e2b..f824fd4cb 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,30 +1,30 @@ { "dist/react-jss.js": { - "bundled": 237212, - "minified": 65434, - "gzipped": 17994 + "bundled": 237652, + "minified": 65587, + "gzipped": 18039 }, "dist/react-jss.min.js": { - "bundled": 201910, - "minified": 56081, - "gzipped": 15810 + "bundled": 202343, + "minified": 56230, + "gzipped": 15853 }, "dist/react-jss.cjs.js": { - "bundled": 17646, - "minified": 7319, - "gzipped": 2579 + "bundled": 18359, + "minified": 7518, + "gzipped": 2649 }, "dist/react-jss.esm.js": { - "bundled": 17160, - "minified": 6889, - "gzipped": 2485, + "bundled": 17873, + "minified": 7088, + "gzipped": 2552, "treeshaked": { "rollup": { - "code": 1908, + "code": 2057, "import_statements": 214 }, "webpack": { - "code": 3079 + "code": 3228 } } } diff --git a/package-lock.json b/package-lock.json index 4d0fdf905..3bd21571f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" } }, "isobject": { @@ -40,7 +40,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "requires": { - "js-tokens": "4.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" } }, "object-assign": { @@ -53,8 +53,8 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "requires": { - "loose-envify": "1.4.0", - "object-assign": "4.1.1" + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" } }, "theming": { @@ -62,10 +62,10 @@ "resolved": "https://registry.npmjs.org/theming/-/theming-1.3.0.tgz", "integrity": "sha512-ya5Ef7XDGbTPBv5ENTwrwkPUexrlPeiAg/EI9kdlUAZhNlRbCdhMKRgjNX1IcmsmiPcqDQZE6BpSaH+cr31FKw==", "requires": { - "brcast": "3.0.1", - "is-function": "1.0.1", - "is-plain-object": "2.0.4", - "prop-types": "15.6.2" + "brcast": "^3.0.1", + "is-function": "^1.0.1", + "is-plain-object": "^2.0.1", + "prop-types": "^15.5.8" } } } diff --git a/src/JssProvider.js b/src/JssProvider.js index c48fedb9a..946b3d7a5 100644 --- a/src/JssProvider.js +++ b/src/JssProvider.js @@ -1,11 +1,23 @@ -import {Component, Children} from 'react' +// @flow +import {Component, Children, type Element, type ElementType} from 'react' import PropTypes from 'prop-types' +import type {Jss, GenerateClassName, SheetsRegistry} from 'jss' import {createGenerateClassNameDefault} from './jss' import * as ns from './ns' import contextTypes from './contextTypes' import propTypes from './propTypes' +import type {Context} from './types' -export default class JssProvider extends Component { +type Props = { + jss?: Jss, + registry?: SheetsRegistry, + generateClassName?: GenerateClassName, + classNamePrefix?: string, + disableStylesGeneration?: boolean, + children: Element +} + +export default class JssProvider extends Component { static propTypes = { ...propTypes, generateClassName: PropTypes.func, @@ -22,7 +34,7 @@ export default class JssProvider extends Component { // 1. Check if there is a value passed over props. // 2. If value was passed, we set it on the child context. // 3. If value was not passed, we proxy parent context (default context behaviour). - getChildContext() { + getChildContext(): Context { const { registry, classNamePrefix, @@ -31,7 +43,7 @@ export default class JssProvider extends Component { disableStylesGeneration } = this.props const sheetOptions = this.context[ns.sheetOptions] || {} - const context = {[ns.sheetOptions]: sheetOptions} + const context: Context = {[ns.sheetOptions]: sheetOptions} if (registry) { context[ns.sheetsRegistry] = registry @@ -73,6 +85,12 @@ export default class JssProvider extends Component { return context } + registry: SheetsRegistry + + managers: {} + + generateClassName: GenerateClassName + render() { return Children.only(this.props.children) } diff --git a/src/compose.js b/src/compose.js index 57f9cb1cd..39c769e6e 100644 --- a/src/compose.js +++ b/src/compose.js @@ -1,3 +1,5 @@ +// @flow +import type {Classes} from 'jss' /** * Adds `composes` property to each top level rule * in order to have a composed class name for dynamic style sheets. @@ -21,7 +23,7 @@ * @param {Object} styles dynamic styles object without static properties * @return {Object|null} */ -export default (staticClasses, dynamicClasses) => { +export default (staticClasses: Classes, dynamicClasses: Classes) => { const combinedClasses = {...staticClasses} for (const name in dynamicClasses) { diff --git a/src/contextTypes.js b/src/contextTypes.js index 419f3884d..bfdcf1b0e 100644 --- a/src/contextTypes.js +++ b/src/contextTypes.js @@ -1,3 +1,4 @@ +// @flow import PropTypes from 'prop-types' import * as ns from './ns' import propTypes from './propTypes' diff --git a/src/createHoc.js b/src/createHoc.js index 4004ec50e..d9bbab694 100644 --- a/src/createHoc.js +++ b/src/createHoc.js @@ -1,11 +1,22 @@ -import React, {Component} from 'react' +// @flow +import React, {Component, type ComponentType} from 'react' import PropTypes from 'prop-types' import defaultTheming from 'theming' +import type {StyleSheet} from 'jss' import jss, {getDynamicStyles, SheetsManager} from './jss' import compose from './compose' import getDisplayName from './getDisplayName' import * as ns from './ns' import contextTypes from './contextTypes' +import type { + Options, + Theme, + StylesOrThemer, + InnerProps, + OuterProps, + Context, + SubscriptionId +} from './types' const env = process.env.NODE_ENV @@ -32,7 +43,13 @@ const dynamicStylesNs = Math.random() * */ -const getStyles = (stylesOrCreator, theme) => { +type State = { + theme: Theme, + dynamicSheet?: StyleSheet, + classes: {} +} + +const getStyles = (stylesOrCreator: StylesOrThemer, theme: Theme) => { if (typeof stylesOrCreator !== 'function') { return stylesOrCreator } @@ -57,41 +74,53 @@ let managersCounter = 0 /** * Wrap a Component into a JSS Container Component. * + * InnerPropsType: Props of the InnerComponent. + * OuterPropsType: The Outer props the HOC accepts. + * * @param {Object|Function} stylesOrCreator * @param {Component} InnerComponent * @param {Object} [options] * @return {Component} */ -export default (stylesOrCreator, InnerComponent, options = {}) => { +export default function createHOC< + InnerPropsType: InnerProps, + InnerComponentType: ComponentType, + OuterPropsType: OuterProps +>( + stylesOrCreator: StylesOrThemer, + InnerComponent: InnerComponentType, + options: Options +): ComponentType { const isThemingEnabled = typeof stylesOrCreator === 'function' const {theming = defaultTheming, inject, jss: optionsJss, ...sheetOptions} = options const injectMap = inject ? toMap(inject) : defaultInjectProps const {themeListener} = theming const displayName = getDisplayName(InnerComponent) - const defaultClassNamePrefix = env === 'production' ? undefined : `${displayName}-` + const defaultClassNamePrefix = env === 'production' ? '' : `${displayName}-` const noTheme = {} const managerId = managersCounter++ const manager = new SheetsManager() - const defaultProps = {...InnerComponent.defaultProps} + // $FlowFixMe defaultProps are not defined in React$Component + const defaultProps: InnerPropsType = {...InnerComponent.defaultProps} delete defaultProps.classes - class Jss extends Component { + class Jss extends Component { static displayName = `Jss(${displayName})` static InnerComponent = InnerComponent static contextTypes = { ...contextTypes, - ...(isThemingEnabled && themeListener.contextTypes) + ...(isThemingEnabled ? themeListener.contextTypes : {}) } static propTypes = { innerRef: PropTypes.func } static defaultProps = defaultProps - constructor(props, context) { + constructor(props: OuterPropsType, context: Context) { super(props, context) const theme = isThemingEnabled ? themeListener.initial(context) : noTheme - this.state = this.createState({theme}, props) + this.state = this.createState({theme, classes: {}}, props) } componentWillMount() { @@ -104,13 +133,13 @@ export default (stylesOrCreator, InnerComponent, options = {}) => { } } - componentWillReceiveProps(nextProps, nextContext) { + componentWillReceiveProps(nextProps: OuterPropsType, nextContext: Context) { this.context = nextContext const {dynamicSheet} = this.state if (dynamicSheet) dynamicSheet.update(nextProps) } - componentWillUpdate(nextProps, nextState) { + componentWillUpdate(nextProps: OuterPropsType, nextState: State) { if (isThemingEnabled && this.state.theme !== nextState.theme) { const newState = this.createState(nextState, nextProps) this.manage(newState) @@ -119,7 +148,7 @@ export default (stylesOrCreator, InnerComponent, options = {}) => { } } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: OuterPropsType, prevState: State) { // We remove previous dynamicSheet only after new one was created to avoid FOUC. if (prevState.dynamicSheet !== this.state.dynamicSheet && prevState.dynamicSheet) { this.jss.removeStyleSheet(prevState.dynamicSheet) @@ -127,19 +156,21 @@ export default (stylesOrCreator, InnerComponent, options = {}) => { } componentWillUnmount() { - if (this.unsubscribeId) { + if (isThemingEnabled && this.unsubscribeId) { themeListener.unsubscribe(this.context, this.unsubscribeId) } this.manager.unmanage(this.state.theme) if (this.state.dynamicSheet) { - this.state.dynamicSheet.detach() + this.jss.removeStyleSheet(this.state.dynamicSheet) } } - setTheme = theme => this.setState({theme}) + setTheme = (theme: Theme) => this.setState({theme}) + unsubscribeId: SubscriptionId + context: Context - createState({theme, dynamicSheet}, {classes: userClasses}) { + createState({theme, dynamicSheet}: State, {classes: userClasses}): State { const contextSheetOptions = this.context[ns.sheetOptions] if (contextSheetOptions && contextSheetOptions.disableStylesGeneration) { return {theme, dynamicSheet, classes: {}} @@ -161,9 +192,11 @@ export default (stylesOrCreator, InnerComponent, options = {}) => { classNamePrefix }) this.manager.add(theme, staticSheet) + // $FlowFixMe Cannot add random fields to instance of class StyleSheet staticSheet[dynamicStylesNs] = getDynamicStyles(styles) } + // $FlowFixMe Cannot access random fields on instance of class StyleSheet const dynamicStyles = staticSheet[dynamicStylesNs] if (dynamicStyles) { @@ -176,6 +209,7 @@ export default (stylesOrCreator, InnerComponent, options = {}) => { }) } + // $FlowFixMe InnerComponent can be class or stateless, the latter doesn't have a defaultProps property const defaultClasses = InnerComponent.defaultProps ? InnerComponent.defaultProps.classes : undefined @@ -194,7 +228,7 @@ export default (stylesOrCreator, InnerComponent, options = {}) => { return {theme, dynamicSheet, classes} } - manage({theme, dynamicSheet}) { + manage({theme, dynamicSheet}: State) { const contextSheetOptions = this.context[ns.sheetOptions] if (contextSheetOptions && contextSheetOptions.disableStylesGeneration) { return @@ -231,8 +265,7 @@ export default (stylesOrCreator, InnerComponent, options = {}) => { render() { const {theme, dynamicSheet, classes} = this.state - const {innerRef, ...props} = this.props - + const {innerRef, ...props}: OuterPropsType = this.props const sheet = dynamicSheet || this.manager.get(theme) if (injectMap.sheet && !props.sheet) props.sheet = sheet diff --git a/src/getDisplayName.js b/src/getDisplayName.js index 9b69653a6..cde4c41ba 100644 --- a/src/getDisplayName.js +++ b/src/getDisplayName.js @@ -1 +1,5 @@ -export default Component => Component.displayName || Component.name || 'Component' +// @flow +import type {ComponentType} from 'react' + +export default

(Component: ComponentType

) => + Component.displayName || Component.name || 'Component' diff --git a/src/index.js b/src/index.js index 0e3587e83..a19f493ff 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +// @flow export {ThemeProvider, withTheme, createTheming} from 'theming' export {default as JssProvider} from './JssProvider' export { diff --git a/src/injectSheet.js b/src/injectSheet.js index d9234376f..93f273ea8 100644 --- a/src/injectSheet.js +++ b/src/injectSheet.js @@ -1,5 +1,8 @@ +// @flow +import type {ComponentType, Node} from 'react' import hoistNonReactStatics from 'hoist-non-react-statics' import createHoc from './createHoc' +import type {Options, StylesOrThemer, InnerProps} from './types' /** * Global index counter to preserve source order. @@ -15,7 +18,7 @@ import createHoc from './createHoc' */ let indexCounter = -100000 -const NoRenderer = ({children}) => children || null +const NoRenderer = (props: {children?: ?Node}) => props.children || null /** * HOC creator function that wrapps the user component. @@ -24,12 +27,14 @@ const NoRenderer = ({children}) => children || null * * @api public */ -export default function injectSheet(stylesOrSheet, options = {}) { +export default function injectSheet(stylesOrSheet: StylesOrThemer, options?: Options = {}) { if (options.index === undefined) { options.index = indexCounter++ } - return (InnerComponent = NoRenderer) => { + + return (InnerComponent: ComponentType = NoRenderer) => { const Jss = createHoc(stylesOrSheet, InnerComponent, options) + return hoistNonReactStatics(Jss, InnerComponent, {inner: true}) } } diff --git a/src/jss.js b/src/jss.js index c13ebd2c6..251b296c0 100644 --- a/src/jss.js +++ b/src/jss.js @@ -1,3 +1,4 @@ +// @flow import {create} from 'jss' import preset from 'jss-preset-default' @@ -7,5 +8,6 @@ export { SheetsManager, createGenerateClassName as createGenerateClassNameDefault } from 'jss' +export type {StyleSheet} from 'jss' export default create(preset()) diff --git a/src/ns.js b/src/ns.js index d7bf9cbd5..c7337cb66 100644 --- a/src/ns.js +++ b/src/ns.js @@ -1,3 +1,4 @@ +// @flow /** * Namespaces to avoid conflicts on the context. */ diff --git a/src/propTypes.js b/src/propTypes.js index 7aed6ac21..62c188519 100644 --- a/src/propTypes.js +++ b/src/propTypes.js @@ -1,3 +1,4 @@ +// @flow import PropTypes from 'prop-types' export default { diff --git a/src/types.js b/src/types.js new file mode 100644 index 000000000..14cdfd5dd --- /dev/null +++ b/src/types.js @@ -0,0 +1,43 @@ +// @flow +import type {StyleSheetFactoryOptions, Jss, SheetsRegistry, SheetsManager} from 'jss' +import type {ComponentType, Node, ElementRef} from 'react' + +export type Theme = {} +export type SubscriptionId = string +type Theming = { + channel: string, + withTheme:

(comp: ComponentType

) => ComponentType

, + ThemeProvider: ComponentType<{ + theme: Theme | ((outerTheme: Theme) => Theme), + children: Node + }>, + themeListener: { + contextTypes: {}, + initial: (context: {}) => Theme, + subscribe: (context: {}, cb: (theme: Theme) => void) => SubscriptionId, + unsubscribe: (context: {}, id: SubscriptionId) => void + } +} + +export type Options = { + theming?: Theming, + inject?: Array<'classes' | 'themes' | 'sheet'>, + jss?: Jss +} & StyleSheetFactoryOptions +export type InnerProps = { + children?: Node, + classes?: {}, + theme?: Theme, + sheet?: {} +} +// Needs to be hard coded for stricter types +export type Context = { + '64a55d578f856d258dc345b094a2a2b3'?: Jss, + d4bd0baacbc52bbd48bbb9eb24344ecd?: SheetsRegistry, + b768b78919504fba9de2c03545c5cd3a?: {[key: number]: SheetsManager}, + '6fc570d6bd61383819d0f9e7407c452d': StyleSheetFactoryOptions & {disableStylesGeneration?: boolean} +} +export type OuterProps = IP & {innerRef: (instance: ElementRef | null) => void} +export type Styles = {[string]: {}} +export type ThemerFn = (theme: Theme) => Styles +export type StylesOrThemer = Styles | ThemerFn