Skip to content
This repository has been archived by the owner on Feb 14, 2023. It is now read-only.

Class components ignore Providers in newer versions of React. #132

Open
vjsingh opened this issue Oct 9, 2019 · 6 comments
Open

Class components ignore Providers in newer versions of React. #132

vjsingh opened this issue Oct 9, 2019 · 6 comments
Labels
bug Something isn't working as expected. help wanted The owner cannot resolve this on their own.

Comments

@vjsingh
Copy link

vjsingh commented Oct 9, 2019

I'm using Expo with a web project and a native project that are both part of the same package / project / repo.

Is there any suggested way to structure multiple reactn stores for Typescript - one for the web and one for native? Using 'global.d.ts' you can't have multiple store type definitions it seems like.

The obvious answer seems to be to use Providers instead, but just curious if anyone else has come across this use case. I don't see documentation in the Providers readme on how to structure the typescript type definitions

EDIT: I also tried conditionally declaring the module by using a conditional on react-native Platform, but that didn't seem to work either.

@quisido
Copy link
Collaborator

quisido commented Oct 10, 2019

A Provider would be the solution to having two stores in one code base. You do not need a global.d.ts file for a Provider, because TypeScript can infer the shape of your global state based on the Provider object itself. This is in contrast to the default global state, which exists as a mutated module, and the global.d.ts exists to tell it the mutated shape.

// empty default state module
state = { };
// TypeScript believes the state to be {}

// setup file
state.x = true;
// TypeScript believes the state to be {}, because TypeScript does not infer runtime code

// Provider/Store
const x = createProvider({ shape });
// TypeScript believes the state to be { shape }

// consumer
const state = x.getGlobal();
// TypeScript believes the state to be { shape }

Hope this makes sense.

Since a Provider is created by passing a typed state object, TypeScript infers that the Provider's shape is the same as that state object going forward.

const Provider: ReactNProvider<T> = createProvider<T>(defaultState as T);

The default global state needing a global.d.ts file is the exception to the rule, a limitation of TypeScript specifically when dealing with mutating objects from their instantiated shape.

Providers are instantiated as the correct shape and therefore do not need their TypeScript definitions overridden. Whatever the type of the object that you pass to createProvider is will be the type of the global state returned by all of that Provider's methods.

Let me know if yo need more assistance.

@quisido quisido added the support A user is requesting support. label Oct 10, 2019
@vjsingh
Copy link
Author

vjsingh commented Oct 23, 2019

Thanks @CharlesStover, switching over to the Provider model did it.

I wonder if it's worth calling this out a little more explicitly? If this is the recommended model for having a web and native codebase, and there's a chance that someone is going to eventually have both, it seems worthwhile to start with the Provider model vs the hassle of switching later.

@vjsingh vjsingh closed this as completed Oct 23, 2019
@vjsingh
Copy link
Author

vjsingh commented Oct 23, 2019

@CharlesStover Actually, I got it working for my functional components, but I'm having two issues when trying to use the new Provider model with my class-based components:

  • this.global returns {} initially instead of the initial object passed in to createProvider
  • Typescript can't figure out what the shape of this.global is (Property 'userId' does not exist on type 'Readonly<State>'.)

@vjsingh vjsingh reopened this Oct 23, 2019
@quisido
Copy link
Collaborator

quisido commented Oct 24, 2019

For the former, how do you mean "returns {} initially"? As in it is {} but on the next rerender it is the correct global object? Or does this.global just always return an empty object for you when using class components?

There is no way to fix the latter. TypeScript can have no knowledge of which Provider is parent to the component. As an example:

import { Component } from 'reactn';
class MyComponent extends Component {
  render() {
    return Math.floor(this.global.x); // x is a number
  }
}

// outputs: 1
const Provider1 = createProvider({ x: 1.23 });
<Provider1><MyComponent /></Provider1>

// Should be a TypeScript error, but TypeScript cannot know:
const Provider2 = createProvider({ x: 'string' });
<Provider2><MyComponent /></Provider2>

In the above scenario, how would MyComponent know that x can be number or string? Or how can TypeScript throw an error if you attempt to mount a component whose expected global state does not match the one provided by context? There just isn't a way to do that.

You may have to use as unknown as MyState or as any as MyState to force a correct type.

@quisido quisido changed the title Using in two platforms - web and native Using multiple Providers. Oct 24, 2019
@vjsingh
Copy link
Author

vjsingh commented Oct 25, 2019

@CharlesStover For the latter, that's right. I had the same intuition about TS not being able to know the type of the global state in this case, but since it wasn't mentioned in the doc I just wanted to be sure. Typecasting works well.

For the first issue, this.global always returns an empty object for me, even though I'm initializing a provider with createProvider and wrapping my React app JSX in it . If I call setGlobal explicitly in the component then this.global changes, though.

I think I'm setting things up right since hooks work great, but copied the code below:

Store.tsx:

import * as React from 'react';
import { createProvider, setGlobal, useDispatch } from 'reactn';
import { State } from 'reactn/default';

const initialStore = {
    snackbar: {
      text: '',
    },
};
export type StoreType = typeof initialStore;

function makeStoreProvider() {
  return createProvider(initialStore);
}

const Store = makeStoreProvider();
export default Store;

App.tsx:

import Store from 'shared/Util/Store';

            <Store>
                <RootNavigator AppMain={AppMain}/>
            </Store>

AComponent.tsx:

import React from 'reactn';
import Store, { StoreType } from 'shared/Util/Store';

  public render() {
     let global = this.global as StoreType;
     console.log(global);  // Always prints {} unless I call this.setGlobal

@quisido
Copy link
Collaborator

quisido commented Nov 30, 2019

Sorry for the late reply. You're right. It looks like for newer versions of React, class components are not correctly able to access the global state via the context when the global state is coming from a Provider.

I'm not sure there is a backwards compatible way of handling this, since the new context API does not support old contexts, and the old context API does not support new contexts! It is difficult to write a class component that hooks into all possible React contexts.

I'll document this for the time being as a known issue. Thank you for reporting it.

@quisido quisido added bug Something isn't working as expected. and removed support A user is requesting support. labels Nov 30, 2019
@quisido quisido changed the title Using multiple Providers. Class components ignore Providers in newer versions of React. Nov 30, 2019
@quisido quisido added the help wanted The owner cannot resolve this on their own. label Nov 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working as expected. help wanted The owner cannot resolve this on their own.
Projects
None yet
Development

No branches or pull requests

2 participants