Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(context): Update to latest React Context API #391

Merged
merged 17 commits into from
Sep 6, 2018

Conversation

derrickhnguyen
Copy link
Contributor

@derrickhnguyen derrickhnguyen commented Aug 28, 2018

Description

The React team will not support the legacy context API in React 17. To keep up-to-date with latest changes and improvements from the React team, we must transition all usage of the legacy context API with the newest one.

How Has This Been Tested?

  • Unit tests

src/defaults.js Outdated
@@ -60,4 +59,4 @@ export default {
'2XL': 6,
},
verticalGutter: 3, // value (in base units) that separates columns vertically
}
}: Object)
Copy link
Contributor Author

@derrickhnguyen derrickhnguyen Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm conflicted with this. I tried converting this into $NonMaybeType<ConfigContextType> but then withResolution.logic.js complains with a Flow error of: undefined is incompatible with number, which is ironic since i was hoping $NonMaybeType<> would fix that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried with $Shape<ConfigContextType> and { ...ConfigContextType }?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did. I get this error:

Cannot assign object literal to defaults because:
 • property base is not writable in property base.
 • property columns is not writable in property columns.
 • property displayAliases is not writable in property displayAliases.
 • property fallbackDisplayKey is not writable in property fallbackDisplayKey.
 • property gutter is not writable in property gutter.
 • property maxPageWidth is not writable in property maxPageWidth.
 • property minPageWidth is not writable in property minPageWidth.
 • property pageMargin is not writable in property pageMargin.
 • property spacingAliases is not writable in property spacingAliases.
 • property verticalGutter is not writable in property verticalGutter.

      1│ // @flow
      2│ import { type ConfigContextType } from './types'
      3│
      4│ const defaults: $Shape<ConfigContextType> = {
      5│   base: 8, // multiplier (in pixels) that all other size units use
      6│   columns: 12, // number of columns used in the layout
      7│   displayAliases: {
      8│     // aliases used for the different display breakpoints:
      9│     small: [
     10│       {
     11│         //   - "small" alias used when width is less than 600px
     12│         maxWidth: '599px',
     13│       },
     14│       {
     15│         maxDeviceWidth: '599px',
     16│       },
     17│     ],
     18│     medium: [
       :
     50│     // aliases used to indicate spacing values (margin/padding) in base
     51│     XS: 0.5, // units.
     52│     'S/2': 0.5,
     53│     S: 1,
     54│     'M/2': 1,
     55│     M: 2,
     56│     'L/2': 1.5,
     57│     L: 3,
     58│     'XL/2': 2,
     59│     XL: 4,
     60│     '2XL/2': 3,
     61│     '2XL': 6,
     62│   },
     63│   verticalGutter: 3, // value (in base units) that separates columns vertically
     64│ }
     65│
     66│ export default defaults
     67│
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/utils/index.js:193:12

undefined [1] is incompatible with number [2].

     src/utils/index.js
 [2] 185│   gutter: number,
        :
     190│   spacingProps,
     191│   base,
     192│   spacingAliases,
     193│   gutter = defaults.gutter,
     194│   verticalGutter = defaults.verticalGutter,
     195│ }: CombineSpacingSettings) {
     196│   const combinedSpacingAliases = {

     src/types.js
 [1]  56│   +gutter?: number,


Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/utils/index.js:194:20

undefined [1] is incompatible with number [2].

     src/utils/index.js
 [2] 186│   verticalGutter: number,
        :
     191│   base,
     192│   spacingAliases,
     193│   gutter = defaults.gutter,
     194│   verticalGutter = defaults.verticalGutter,
     195│ }: CombineSpacingSettings) {
     196│   const combinedSpacingAliases = {
     197│     ...defaults.spacingAliases,

     src/types.js
 [1]  63│   +verticalGutter?: number,


Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/withResolution/withResolution.logic.js:51:24

undefined [1] is incompatible with string [2].

     src/withResolution/withResolution.logic.js
     48│   props,
     49│   shouldShow,
     50│   resolutionKeys = [],
     51│   fallbackDisplayKey = defaults.fallbackDisplayKey,
     52│ }: {|
     53│   +props: *,
     54│   +shouldShow?: ShouldShow,
     55│   +resolutionKeys: Array<string>,
 [2] 56│   +fallbackDisplayKey: string,

     src/types.js
 [1] 55│   +fallbackDisplayKey?: string,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arahansen do you have other suggestions?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this worked for me:

{|
  ...ConfigContextType,
  gutter: number,
  fallbackDisplayKey: string,
  verticalGutter: number,
|}

@@ -46,8 +46,6 @@ module.exports = {
devtool: 'source-map',
externals: {
react: 'react',
'react-dom': 'react-dom',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is react-dom still needed here then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I'll add this back.

@obartra
Copy link
Collaborator

obartra commented Sep 5, 2018

This is a breaking change, make sure the combined commit says "BREAKING CHANGE" on the last line following conventional changelog format (otherwise there won't be a major version increase)

export default {
Provider: ConfigProvider,
Consumer: ConfigConsumer,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why export both? We only need ConfigConsumer externally (the provider is only used internally, I would not export it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense. I'll make Provider only exportable inside it's own file but not in gymnast.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I was working on removing this, I noticed (via Storybook) that when using <ConfigProvider> imported from a relative path alongside a component (i.e. <Grid>) imported from a gymnast import, that <Grid> will not use the same React.createContext() as provided from <ConfigProvider> when imported from a relative path.

This is only an issue in Storybook in the Config Provider section. However, this shouldn't be a problem in production, since we're only exporting Consumer, which should be using the same React.createcontext() as the Provider imported from gymnast.

Example:

// @flow
// storybook/stories/configProvider/columns.js

import * as React from 'react'
import { times } from 'lodash'
import { number } from '@storybook/addon-knobs'
import { Grid } from 'gymnast'
import { colors } from '../../shared'

// This is using a different `React.createContext()` than the imported
// Grid above. Therefore, Grid isn't going to consume the 'columns' that
// was passed through from this ConfigProvider, and instead, will used
// the default columns value.
import ConfigProvider from '../../../src/configContext/configProvider'

export default () => {
  const columns = number('Columns', 10, { range: true, min: 1, max: 24 })
  const items = number('Items', 20, { range: true, min: 1, max: 48 })

  return (
    <ConfigProvider columns={columns}>
      <Grid padding="0 L/2">
        {times(items, key => (
          <Grid key={key} padding="0 L/2 L" size={1}>
            <Grid
              style={colors.colors1}
              padding="L/2"
              align="center"
              justify="center"
            >
              {key + 1}
            </Grid>
          </Grid>
        ))}
      </Grid>
    </ConfigProvider>
  )
}

This appears to be a known issue via this issue

To alleviate the Storybook problem, I can remove all imports from gymnast and just imported via relative path, like so:

// @flow
// storybook/stories/configProvider/columns.js

import * as React from 'react'
import { times } from 'lodash'
import { number } from '@storybook/addon-knobs'
import { colors } from '../../shared'
import ConsumerProvider from '../../../src/configContext/configProvider'
import Grid from '../../../src/grid'

export default () => {
  const columns = number('Columns', 10, { range: true, min: 1, max: 24 })
  const items = number('Items', 20, { range: true, min: 1, max: 48 })

  return (
    <ConsumerProvider columns={columns}>
      <Grid padding="0 L/2">
        {times(items, key => (
          <Grid key={key} padding="0 L/2 L" size={1}>
            <Grid
              style={colors.colors1}
              padding="L/2"
              align="center"
              justify="center"
            >
              {key + 1}
            </Grid>
          </Grid>
        ))}
      </Grid>
    </ConsumerProvider>
  )
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a babel alias to it to make it a bit more convenient to use. If we define one (for stories only) that maps gymnastSrc to the src path we could then do something like gymnastSrc/configContext/configProvider

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to ensure the project is still tree-shakeable with babel-plugin-lodash this index.js file needs to contain as default export the same value we export through src/index.js

<TesterComponent>{render2}</TesterComponent>
</ConfigContext.Provider>
<TesterComponent>{render3}</TesterComponent>
</ConfigContext.Provider>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 nice test cases

const contextValues = get(context, 'gymnast', {})

return { ...defaults, ...contextValues, ...overrides }
return { ...defaults, ...context, ...overrides }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method looks so nice now ✨

@@ -3,7 +3,10 @@
exports[`e2e tests should export the same values from the main export than individual files 1`] = `
Object {
"Col": [Function],
"ConfigProvider": [Function],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the update to not export config context, will we keep the same API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once I make my changes, it will look like:

Object {
  "Col": [Function],
  "ConfigContext": Object {
    "Consumer": [Function],
  },

or another option to create a separate folder for configConsumer, then the snapshot could look like:

Object {
  "Col": [Function],
  "ConfigConsumer": [Function],

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not keep the API the same? if we change it we'll have to have a separate v1 vs v0 docs

Copy link
Contributor Author

@derrickhnguyen derrickhnguyen Sep 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be confusing to name it ConfigProvider, but only export a ConfigConsumer?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internally you see it as Provider / Consumer because we are implementing it using React Context API but the gymnast consumer should not be aware of these details.

Here we are exposing a ConfigProvider for the user to customize gymnast (a Component they can use to provide configuration options).

It's a common pattern (both in naming and behavior) that Redux and Apollo use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, the way Redux and Apollo are exposing their context is by externally exporting their Provider component while leaving their Consumer component un-exported, but providing a HOC component to access the context value (i.e. connect() in Redux and <Query> in Apollo).

So should your comment via src/configContext/index.js be reversed where we should externally export our Provider and not our Consumer?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whichever one we export here should be the one exported as default on configContext/index.js or the lodash plugin will export the wrong thing

src/gymnast.js Outdated
@@ -3,7 +3,7 @@ import './cxs'

// Components
export { default as Col } from './col'
export { default as ConfigProvider } from './configProvider'
export { default as ConfigContext } from './configContext'
export { default as Grid } from './grid'
export { default as Layout } from './layout'
export { default as Offset } from './offset'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we export withContext for consistency as well?

}
}

state = {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to keep this here, since getting warning via yarn test:

$ jest -u
 PASS  src/asCol/asCol.spec.js
 PASS  src/asLayout/layout.spec.js
 PASS  src/offset/offset.spec.js
 PASS  src/root/root.spec.js
 PASS  src/dev/dev.spec.js
 PASS  src/withResolution/withResolution.logic.spec.js
 PASS  src/core/core.spec.js
 PASS  src/withResolution/withResolution.spec.js
 PASS  src/log/log.spec.js
 PASS  src/withResolution/mediaQuery.spec.js
 PASS  src/errors/index.spec.js
 PASS  src/utils/utils.spec.js
 PASS  src/asGrid/grid.spec.js
 PASS  src/configProvider/index.spec.js
  ● Console

    console.error node_modules/fbjs/lib/warning.js:33
      Warning: ConfigProvider: Did not properly initialize state during construction. Expected state to be an object, but it was undefined.

 PASS  test/e2e/gymnast.spec.js
 PASS  src/withContext/index.spec.js
  ● Console

    console.error node_modules/fbjs/lib/warning.js:33
      Warning: ConfigProvider: Did not properly initialize state during construction. Expected state to be an object, but it was undefined.


Test Suites: 16 passed, 16 total
Tests:       3 skipped, 118 passed, 121 total
Snapshots:   4 passed, 4 total
Time:        4.683s
Ran all test suites.
✨  Done in 6.51s.

Copy link
Collaborator

@obartra obartra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks perfect, let's make sure the commit message is correct before merging (so it's released correctly) but after that :shipit: !

🎉

@derrickhnguyen derrickhnguyen merged commit 1d31921 into master Sep 6, 2018
@derrickhnguyen derrickhnguyen deleted the denguyen/new-context-api branch September 6, 2018 19:30
@obartra
Copy link
Collaborator

obartra commented Sep 6, 2018

🎉 This PR is included in version 16.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants