New React context API and flow error, when trying to render Consumer
Asked Answered
C

4

7

When I try to render Consumer, flow show next error:

[flow] Cannot create SidebarContextConsumer element because property changeOpenState is missing in undefined [1] in the first argument of property children. (References: [1])

Here my code:

// @flow
import React, { createContext } from 'react';

import type { Context, ProviderProps } from './Sidebar.types';

const SidebarContext = createContext();

export const SidebarContextConsumer = SidebarContext.Consumer;

/* eslint-disable react/no-unused-state */
export class SidebarContextProvider extends React.Component<ProviderProps, Context> {

  state = {
    dynamic: false,
    open: false,
    transition: false,
    changeDynamicMode: (dynamic: boolean) => {
      this.setState({
        dynamic,
        open: false,
        transition: false,
      });
    },
    changeOpenState: (open: boolean, transition: boolean = true) => {
      this.setState({ open, transition });
    },
  };

  render() {
    const { children } = this.props;

    return (
      <SidebarContext.Provider value={this.state}>
        {children}
      </SidebarContext.Provider>
    );
  }

}
/* eslint-enable */

Flow declaration:

export type Context = {
  changeDynamicMode: (dynamic: boolean) => void,
  changeOpenState: (open: boolean, transition?: boolean) => void,
  dynamic: boolean,
  open: boolean,
  transition: boolean,
};
Chuppah answered 5/6, 2018 at 12:13 Comment(0)
T
9

It seems to be a known limitation of flowtype.

I resolved the error in my case with:

const SidebarContext: Object = createContext();
Tera answered 6/7, 2018 at 20:10 Comment(0)
A
3

TL;DR This can be solved by adding a default to createContext

I think the problem is that Flow there is a logical flaw in how Flow interacts with React.createContext(). First, we can specify the context value by using React.Context<myObject>. Unfortunately if we don't specify a default parameter Flow correctly realizes that the myObject can be undefined.

A simple test case to that you can play around with at Try Flow:

import * as React from 'react';

type obj = {| a: string, b: number |}
const ctxt: React.Context<?obj> = React.createContext();

const cmpnt = () => (
  <ctxt.Provider value={{ a: "a", b: 2 }}>
    <ctxt.Consumer>
      {({a}) => console.log(a.toUpperCase())}
    </ctxt.Consumer>
  </ctxt.Provider>);

As you can see the error in Flow 0.78:

{({a}) => console.log(a.toUpperCase())}
 ^ Cannot create `ctxt.Consumer` element because property `a` is missing in null or undefined [1] in the first argument of property `children`.

References:

4: const ctxt: React.Context<?obj> = React.createContext();

If we add a default it disappears:

import * as React from 'react';

type obj = {| a: string, b: number |}
const ctxt: React.Context<obj> = React.createContext({ a: "test", b: 2 });

const cmpnt = () => (
  <ctxt.Provider value={{ a: "a", b: 2 }}>
    <ctxt.Consumer>
      {({a}) => console.log(a.toUpperCase())}
    </ctxt.Consumer>
  </ctxt.Provider>);

Suggested fix to the original problem

import * as React from 'react';

type Context = {
  changeDynamicMode: (dynamic: boolean) => void,
  changeOpenState: (open: boolean, transition?: boolean) => void,
  dynamic: boolean,
  open: boolean,
  transition: boolean,
};

const defaultState: Context = {
  dynamic: false,
  open: false,
  transition: false,
  changeDynamicMode: (dynamic: boolean) => console.error('Fix function!', dynamic),
  changeOpenState: (open: boolean, transition: boolean = true) => console.error('Fix function!', open, transition),
}

const ctxt = React.createContext(defaultState);

class SidebarContextProvider extends React.Component<mixed & { children: React.Node }, Context> {

  state = {
    dynamic: false,
    open: false,
    transition: false,
    changeDynamicMode: (dynamic: boolean) => {
      this.setState({
        dynamic,
        open: false,
        transition: false,
      });
    },
    changeOpenState: (open: boolean, transition: boolean = true) => {
      this.setState({ open, transition });
    },
  };

  render() {
    const { children } = this.props;

    return (
      <ctxt.Provider value={this.state}>
        {children}
      </ctxt.Provider>
    );
  }
}
Agrestic answered 5/8, 2018 at 9:37 Comment(0)
P
0

The below works for me

export const SidebarContextConsumer = ((SidebarContext.Consumer: any):
  React$ComponentType<{
    children: (value: Context) => ?React$Node
  }>
);

I've casted through any and restored original Consumer typedef but with maybe type of value removed

React$ComponentType<{ children: (value: ?Context) => ?React$Node}>
Polard answered 27/12, 2018 at 9:13 Comment(0)
L
0

With flow, this is how I would usually type context (without initial value):

const SidebarContext = createContext<Context | typeof undefined>();

where Context is your custom defined type.

What also might be useful is using exact type for your Context so flow will throw an error if somebody would try to use attribute/key which is not defined in Context type.

Lawry answered 7/1, 2021 at 22:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.