Class components ignore Providers in newer versions of React.
vjsingh opened this issue · 6 comments
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.
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.
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.
@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>'.
)
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.
@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
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.