I'm currently in the process of migrating a nuxt application with Vuex from JS to TS. I'm starting with one of ou Vuex modules. The thing is that we use a library we wrote at our company and that is used in other projects (can't touch it or migrate it to TS...) to generate some base state, getters, setters, actions and mutations for each module given a certain entity. This library is written is JS with no types available so I'm working on some shims to get some typings in my project. Let me walk you through the whole workflow before I explain my problem.
The library exports a createEntity function that takes a String and returns an object that looks like this:
{
path, //The name of the module
mutationPrefix, //path.uppercase()
actionPrefix, //path
singleSchema, //some normalizr stuff
multipleSchema, //some normalizr stuff
storeKey: 'entities', //the default name for the key in the store
}
We then use the Entity that has been returned to generate the baseState/baseMutations/baseGetters/baseActions of this store module (these are basic CRUD operations on a given entity).
For the module I'm working on (the first I'm migrating to TS), the state.ts file looks like this
import { createState } from '#kissmylabs/vuex-entitystore'
import { Provider } from '~/types/models/Provider'
import { provider } from './entities' // The entity created by our createEntity() library function
export const baseCreateForm = (): Provider => ({
username: '',
email: '',
password: '',
// ... some other form fields
})
export const getBaseState = () => ({
...createState(provider), // The state created by our createState() library function
pagination: {
total: 0,
},
createForm: baseCreateForm(),
isImpersonating: false,
})
export default getBaseState
So I'm trying to implement types for my state, using some TS Interfaces. My interface BaseState (the return type of my createState() library function) looks like this
interface BaseState {
storeKey: {
byId: any,
allIds: Array<number>,
active: Array<any>,
deleted: Array<any>,
isFetching: boolean,
errors: Array<any>,
}
}
This is working mostly fine, my types are working, except for the main storeKey part. This is because the createState function takes the entity.storeKey as the key for the returned state, like so:
export const createState = (entity) => {
return {
[`${entity.storeKey}`]: {
byId: {},
allIds: [],
active: [],
deleted: [],
isFetching: false,
errors: [],
},
}
}
So the whole state generation thing works fine, types are ok and everything, the problem comes when I'm trying to reference my state in a mutation for example. In my mutations.ts I want to be able to do something like
export const mutations = {
...createMutations(provider), // My base mutations, typed fine
ADD_PROVIDER_FILE(state: State, { id, file }: { id: number, file: any }) {
state.entities.byId[id].files.push(file) // The problem is here "Property entity doesn't exist"
},
// More mutations...
}
export default mutations
TS does not recognize the fact that my entities key on the state object can be pretty much anything and is generated dynamically by my createState function, depending on a given entity's storeKey attribute.
So my question is: Is there a way to declare an interface for my BaseState that declares storeKey as an object with any name but still containing the right attributes and types inside ?
What I've tried so far :
[key: string] instead of storeKey as my key in BaseState
Extracted the content of the storeKey object in a new interface and doing something like :
interface BaseStateKeys {
byId: any,
allIds: Array<number>,
active: Array<any>,
deleted: Array<any>,
isFetching: boolean,
errors: Array<any>,
}
interface BaseState {
[key: string]: BaseStateKeys
}
None of this worked.
Anything clever I'm missing ? Thanks for help !
Related
I am having a problem trying to figure out how assign an array of users after an async fetch. I have a simple interface User, which has an id and a name:
export interface User {
id: number
}
I then have a UserState that includes an array of users:
export interface UserState {
loading: boolean
users: User[]
status: userStatus
error: string | null | undefined
}
In my slice, I have an asychronous function:
export const getUsers = createAsyncThunk(
'user/fetchUsers',
async (users: []) => {
const response = await fetchUsers();
return response.data;
}
);
I don't have reducers but I do have extraReducers:
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(getUsers.fulfilled, (state, {payload: users}) => {
state.loading = false
state.status = 'idle'
state.users = users //??? Or something like this...???
state.error = ''
})
}
})
For clarity, I've just included this one addCase where I am trying to assign the expected array of users in JSON format into the users array. I've tried a number of different syntax variations and I'm getting errors like:
Type 'User[]' is not assignable to type '(() => Element)[]'.
Type 'User' is not assignable to type '() => Element'.
Type 'User' provides no match for the signature '(): Element'.ts(2322)
I'm not entirely sure how to address this one. I've looked at a number of tutorials and examples but there are many different ways of doing things. I'm not sure if I'm close to a solution -- there is a simple and direct fix from where I am -- or if I'm a world away from fixing this. I feel as if I've gone down too many rabbit holes and ended up creating a monster of mixed parts that don't quite fit together.
Thanks for any help!
From the error message, it seems as if you are trying to assign a component type to the User interface here - it seems you have at some point been importing a User component instead of your User interface. Going through your imports and finding the wrong import will fix your problem.
I would like to create an array tuple with values ['account', 'accountOne'], that are using existing Types, but second tuple value should contain values based on selection of the first one.
Some example code below:
interface RootState {
account: Account;
movies: Movies;
config: Config;
}
interface Account {
accountOne: 'something',
accountTwo: '1'
}
interface Movies {
moviesOne: 'something',
moviesTwo: 'something'
}
interface Config {
configOne: 'something',
configTwo: '123'
}
export type ModuleProp = keyof RootState;
// This is some pseudo code, to show the idea, but I could not make it work
// It gives error: Tuple type arguments circularly reference themselves
export type ModulesTuple = [ModuleProp, keyof RootState[ModulesTuple[0]]
// It would be used as
function fetchSomething({page: number, vuexModuleProp: ModulesTuple}){
const [module, moduleProp] = vuexModuleProp
// rest of the code
}
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'accountOne'] // ERROR
})
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'moviesOne'] // GOOD TO GO
})
Current code gives error of: Tuple type arguments circularly reference themselves
The idea is that if you select 'account' as first tuple, second selection should be 'accountOne' or 'accountTwo', which are nested keys of other interfaces (Movies, Account, Config) in RootState interface.
Not really sure if that is possible with Typescript, but would greatly appreciate any help!
Use Generics.
interface RootState {
account: Account;
movies: Movies;
config: Config;
}
interface Account {
accountOne: 'something',
accountTwo: '1'
}
interface Movies {
moviesOne: 'something',
moviesTwo: 'something'
}
interface Config {
configOne: 'something',
configTwo: '123'
}
// All available modules
type ModuleKeys = keyof RootState;
// Pick a module interface based on type of `T`
type ModuleProps<T extends ModuleKeys> = T extends 'account'
? Account
: T extends 'movies'
? Movies
: T extends 'config'
? Config
: never;
// Creates a tuple where the second type is based on the first type, where the first type is automatically inferred from `T`.
type ModuleTuple<T extends ModuleKeys> = [T, keyof ModuleProps<T>];
// Guard the `T` type to be one of `ModuleKeys`, where it is passed to `ModuleTuple<T>`, where it creates the conditional tuple.
function fetchSomething<T extends ModuleKeys>({ page, vuexModuleProp }: { page: number, vuexModuleProp: ModuleTuple<T> }): any {
const [moduleKey, moduleProps] = vuexModuleProp;
console.log(page, moduleKey, moduleProps);
}
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'accountOne'] // ERROR
});
fetchSomething({
page: 1,
vuexModuleProp: ['movies', 'moviesOne'] // GOOD TO GO
});
Check the playground.
I'm trying to figure out if there's a way to abstract the data fetching hooks I've created, with Typescripts generics, for my React app. Unfortunately my understanding of Typescript isn't as good as I'd hope so I'm in a pickle.
The hooks I'm using are based on the example given by SWR, the data fetching library I'm using. They have an example of what I'd like to build, in their repository but it's using Axios, while I'm using fetch (or isomorphic-unfetch to be exact).
To be explicit, I have useUsers hook to fetch all the users from my users endpoint, like so:
import useSWR from 'swr';
import fetcher from '../fetcher';
import { User } from '#prisma/client';
type UserHookData = {
appUser?: User;
isLoading: boolean;
isError: any;
};
type UserPayload = {
user?: User;
};
function useUsers(): UserHookData {
const { data, error } = useSWR<UserPayload>('/api/users/', fetcher);
const userData = data?.user;
return {
appUser: userData,
isLoading: !error && !data,
isError: error,
};
}
export default useUsers;
The fetcher function used is copied directly from one of their typescript examples and I'm using Prisma as my ORM so I can access the types through it. The User type has fields like email and name.
I've got another, almost identical hook to fetch a single project from my project end point:
import useSWR from 'swr';
import fetcher from '../fetcher';
import { Project } from '#prisma/client';
type ProjectHookData = {
project: Project;
isLoading: boolean;
isError: any;
};
type ProjectPayload = {
project?: Project;
};
function useProject(id: number): ProjectHookData {
const { data, error } = useSWR<ProjectPayload>(`/api/project/${id}`, fetcher);
const project = data?.project;
return {
project,
isLoading: !error && !data,
isError: error,
};
}
export default useProject;
What I'd like to do is have one hook (let's call it useRequest like in the SWR example) that I can fetch most of the data I need. With regular Javascript, I'd probably write it like this:
function useRequest(url, key){
const { data: payload, error } = UseSWR(url, fetcher);
const data = payload[key]
return {
data,
isLoading: !error && !data,
isError: error
};
}
But how would I do this in Typescript, when I want to use this for multiple different data types? I thought about maybe using generics for this, but I'm unsure if it's the correct way to solve this.
The following hook, using generics, seems to work:
import useSWR from 'swr';
import fetcher from './fetcher';
interface DataPaylaod<T> {
[key: string]: T;
}
interface DataResponse<T> {
data: T;
isLoading: boolean;
isError: any;
}
function useRequest<T>(url: string, key: string): DataResponse<T> {
const { data: payload, error } = useSWR<DataPaylaod<T>>(url, fetcher);
const data = payload ? payload[key] : undefined;
return {
data,
isLoading: !data && !error,
isError: error,
};
}
export default useRequest;
At least it passes tsc and returns the data in the form I'm expecting. If anyone has a more elegant solution, I'd love to see it as well.
i want to set token and refresh token in ngrx and save and use that for every request and when the page was reload not delete data in redux .
i implementation this state for this :
i dispatch data :
this.getUserInformation().toPromise().then(responset => {
if (response.success === true) {
this.store.dispatch(new SetUserInformation({
displayName: responset['result']['displayName'],
userInfo: responset.result.claims,
RefreshTokenStatus:false,
accessToken:response['result']['access_token'],
refreshToken:response['result']['refresh_token']
}))
}
});
in module i definde store :
StoreModule.forFeature('Information', reducer),
EffectsModule.forFeature([])
this is my reducer :
const initialState = adapter.getInitialState({
accessToken: null,
refreshToken: null,
RefreshTokenStatus: false,
userInfo: null,
displayName: null
})
export function reducer(state = initialState, action: TokenAction): TokenState {
switch (action.type) {
case TokenActionTypes.UserInformation:
return adapter.addOne(action.payload, state)
default:
return state;
}
}
and this is my model :
export interface TokenState {
accessToken: string;
refreshToken: string;
RefreshTokenStatus: boolean;
userInfo: string[];
displayName: string;
}
this is my Selector:
export interface State extends fromState.State, EntityState<TokenState> {
UserInfo: TokenState;
}
export const adapter: EntityAdapter<TokenState> = createEntityAdapter<TokenState>();
const getTokenFetureState = createFeatureSelector<TokenState>('Information');
export const getAccessToken=createSelector(
getTokenFetureState,
state => state.accessToken
)
this is action :
export class SetUserInformation implements Action {
readonly type = TokenActionTypes.UserInformation;
constructor(public payload: TokenState) {
}
}
export type TokenAction = SetUserInformation
now i have tow problem :
this.store.pipe(select(fromTokenSelect.getAccessToken)).subscribe(data=>{
console.log(data)
})
A: when i want to get token for use that it return null and show me this error :
#ngrx/entity: The entity passed to the selectId implementation returned undefined. You should probably provide your own selectId implementation. The entity that was passed: Object The selectId implementation: (instance) => instance.id
B: when reload the page it delete data from ngrx.
how can i solve this problem ?
You are using ngrx/entity for non-entity data. Entity is designed to work with arrays of data, where each element has some kind of unique identifier. That is why it is throwing the error: you're feeding it non-entity-compatible data. You need to write a simple reducer which simply sets the state, not one which tries to add data to a non-existent entity.
As for page reload, NgRx will always reset the data, that is the expected behavior. You need to implement localStorage saving if you want to persist data over time.
I've built multiple React functional components using Hooks and Context. Everything works fine. Now I need to write tests for everything. I'm confused about how to move forward with some of them so wanted to reach out to the community.
Actions
Here's a sampling from one of my Actions files:
export const ADD_VEHICLE: 'ADD_VEHICLE' = 'ADD_VEHICLE';
export const UPDATE_VEHICLE: 'UPDATE_VEHICLE' = 'UPDATE_VEHICLE';
type AddVehicleAction = {type: typeof ADD_VEHICLE, isDirty: boolean};
type UpdateVehicleAction = {type: typeof UPDATE_VEHICLE, id: number, propName: string, payload: string | number};
export type VehiclesActions =
| AddVehicleAction
| UpdateVehicleAction;
How am I supposed to test this Actions file? I don't mean in conjunction with anything else, I mean it and only it?
From the comments, it appears I have agreement that there's nothing to test DIRECTLY in this file.
Reducers
Each of my Reducers files is directly connected to and supports a specific Context. Here's a sampling of one of my Reducers files:
import type { VehiclesState } from '../VehiclesContext';
import type { VehiclesActions } from '../actions/Vehicles';
import type { Vehicle } from '../SharedTypes';
import { ADD_VEHICLE,
UPDATE_VEHICLE
} from '../actions/Vehicles';
export const vehiclesReducer = (state: VehiclesState, action: VehiclesActions) => {
switch (action.type) {
case ADD_VEHICLE: {
const length = state.vehicles.length;
const newId = (length === 0) ? 0 : state.vehicles[length - 1].id + 1;
const newVehicle = {
id: newId,
vin: '',
license: ''
};
return {
...state,
vehicles: [...state.vehicles, newVehicle],
isDirty: action.isDirty
};
}
case UPDATE_VEHICLE: {
return {
...state,
vehicles: state.vehicles.map((vehicle: Vehicle) => {
if (vehicle.id === action.id) {
return {
...vehicle,
[action.propName]: action.payload
};
} else {
return vehicle;
}
}),
isDirty: true
};
}
If you wanted to build tests for JUST this Reducers file, what approach would you use? My thought was to render the DOM like this:
function CustomComponent() {
const vehiclesState = useVehiclesState();
const { isDirty,
companyId,
vehicles
} = vehiclesState;
const dispatch = useVehiclesDispatch();
return null;
}
function renderDom() {
return {
...render(
<VehiclesProvider>
<CustomComponent />
</VehiclesProvider>
)
};
}
While this code above does run, I now have the problem that both vehiclesState and dispatch are not accessible within my test code so I'm trying to figure out how to "surface" those within each describe / it construct. Any suggestions would be appreciated.
Contexts
My Contexts follow the same pattern outlined by Kent C. Dodds: https://kentcdodds.com/blog/how-to-use-react-context-effectively - in that the StateContext & DispatchContext are separated, and there's a default state. Given this code pattern and given that I'll already have a separate test file for the Context's Reducers, what specifically could one test for ONLY for the Context?
Same as my comment, I really think you should read redux docs for writing tests so that you get a general idea of what to do.
But since you already have a reducer, you want to write your test case to follow this pattern
you will have at least 1 test per action
each test will have a a "previous state", which will be altered
you will call your reducer, passing the action and the previous state
you will assert your new state is the same as expected
Here's a code example:
it('adds a new car when there are no cars yet', () => {
// you want to put here values that WILL change, so that you don't risk
// a false positive in your unit test
const previousState = {
vehicles: [],
isDirty: false,
};
const state = reducer(previousState, { type: ADD_VEHICLE });
expect(state).toEqual({
vehicles: [{
id: 1,
vin: '',
license: '',
}],
isDirty: true,
});
});
it('adds a new car when there are existing cars already, () => {
// ...
});
I'd also recommend to use action creators rather than directly creating action objects, since it's more readable:
// actions.js
export const addVehicle = () => ({
type: ADD_VEHICLE
})
// reducer.test.js
it('adds a new car when there are no cars yet', () => {
//...
const state = reducer(previousState, actions.addVehicle());