I decided to ask for help, I just cannot get my head around NGRX Entity! (This code was created initially by NX ).
I have followed the NGRX Entity guide, I have also looked at loads of tutorial videos but I still cannot get NGRX Entity updateOne to work.
Getting this error below - I can load the entities into the store with no issue, and these are building my UI fine.
I have an Entity collection of buttons and want to update the Store State of a button when clicked - that's all!
(any ideas why this is not working??)
ERROR TypeError: Cannot read property 'id' of undefined
at http://localhost:4200/vendor.js:83815:26
at Array.filter (<anonymous>)
at updateManyMutably (http://localhost:4200/vendor.js:83811:27)
at updateOneMutably (http://localhost:4200/vendor.js:83801:16)
at Object.operation [as updateOne] (http://localhost:4200/vendor.js:83622:27)
at http://localhost:4200/main.js:1169:28
at http://localhost:4200/vendor.js:88532:26
at reducer (http://localhost:4200/main.js:1173:12)
at http://localhost:4200/vendor.js:87072:20
at combination (http://localhost:4200/vendor.js:86960:37)
This is the code I have so far:
// state
export interface QuickButton {
id: number;
isSelected: boolean;
title: string;
linkUrl: string;
}
// in component
this.store.dispatch( actions.setQuickFilter( evt ) );
// evt = {id: 1, isSelected: true, linkUrl: "", title: "Video"}
// in actions
export const setQuickFilter = createAction(
'[QuickBar] setQuickFilter',
props<{update: Update<QuickButton>}>()
);
// in reducer
export const QUICKBAR_FEATURE_KEY = 'quickBar';
export interface State extends EntityState<QuickButton> {
selectedId?: string | number; // which QuickBar record selected
loaded: boolean; // has the QuickBar list been loaded
error?: string | null; // last none error (if any)
}
export interface QuickBarPartialState {
readonly [QUICKBAR_FEATURE_KEY]: State;
}
export const quickBarAdapter: EntityAdapter<QuickButton> = createEntityAdapter<QuickButton>();
export const initialState = quickBarAdapter.getInitialState({
// set initial required properties
loaded: false,
});
const quickBarReducer = createReducer(
initialState,
on(QuickBarActions.loadQuickBarSuccess, (state, action) =>
quickBarAdapter.addAll( action.quickBar, state )
),
on(QuickBarActions.loadQuickBarFailure, (state, { error }) => ({
...state,
error,
})),
on(QuickBarActions.setQuickFilter, (state, {update}) => {
/// **** This is NOT Working *****
return quickBarAdapter.updateOne( update, state);
}
)
);
export function reducer(state: State | undefined, action: Action) {
return quickBarReducer(state, action);
}
export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = quickBarAdapter.getSelectors();
You're dispatching your action incorrectly.
this.store.dispatch(actions.setQuickFilter(evt));
should be
this.store.dispatch(actions.setQuickFilter({ update: evt }));
Yay!! finally.
This was a real dumb error - from not understanding Entity.
Lots of trial and error & logging to solve this!
Solution:
Change the dispatch call in component from:
this.store.dispatch( actions.setQuickFilter( {update: evt} } ) );
to:
this.store.dispatch( actions.setQuickFilter( {update: {id: evt.id, changes: evt} } ) );
Now all my subscribed features will be able to use the updated values in the buttons to control their own UI elements. Finally!
Related
I have a statically generated Next.js site with frontend only. I have a page pages/feedback/[id]/edit.tsx. Getting this dynamic id in a nested FeedbackForm component & setting defaultValues like so:
export const FeedbackForm = ({ editing }: { editing: boolean }) => {
const router = useRouter()
const { id } = router.query
const feedbackData = getFeedback(id as string)
const { register, handleSubmit } = useForm({
defaultValues: {
title: editing ? feedbackData.title : '',
category: editing ? feedbackData.category : categories[0], // an array from local json file
status: editing ? feedbackData.status : statusList[0], // an array from local json file
description: editing ? feedbackData.description : '',
}
})
// ...
}
The problem is that, initially, the id from router.query is undefined. As I've read in Next.js docs, client side router will take over after hydration and id will be filled with the value. This means that FeedbackForm component renders twice and, initially, getFeedbackData returns undefined (because undefined id was passed as an argument).
So my question is, what is the best way to set defaultValues in this case? Should I even use defaultValues here?
Should I modify getFeedbackData function to return object with empty string values if undefined was passed in?
Should I subscribe to the router object changes and only then fill the form with default values? I saw this being done with reset function in useEffect.
I presume that you use Next.JS from the tags.
Using useEffect and getServerSideProps or getStaticProps can be the solutions.
It depends on what you want to achieve.
If you need SEO, for instance, use getServerSideProps or getStaticProps. If you need a lot of dynamic data on your page, it's more scalable to render your page at run time (SSR), and therefore getServerSideProps would be the preferred method. (just keep in mind that the SEO is not the only reason why you would choose one over another)
Otherwise, useEffect will be enough for your needs.
The following codes are just examples, but you'll get the ideas.
using useEffect
interface FeedbackFormProps {
editing: boolean;
}
type FeedbackData = {
title: string;
category: string;
status: string;
description: string;
};
const FeedbackForm: React.FC<FeedbackFormProps> = ({ editing }) => {
const router = useRouter();
const [feedbackData, setFeedbackData] = useState<FeedbackData | null>(null);
const { register, handleSubmit } = useForm({
defaultValues: {
title: editing && feedbackData?.title ? feedbackData.title : '' ,
category: editing && feedbackData?.category ? feedbackData?.category : categories[0], // an array from local json file
...
}
})
useEffect(() => {
const { id } = router.query;
const fetchFeedback = async () => {
const result = await getFeedback(id as string);
setFeedbackData(result);
};
fetchFeedback();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!feedbackData) return <div>{/* loading spinner etc. */}</div>;
return <div>{/* do something with data */}</div>;
};
export default FeedbackForm;
using getServerSideProps
interface FeedbackFormProps {
feedbackData: FeedbackData;
editing: boolean;
}
type FeedbackData = {
title: string;
category: string;
status: string;
description: string;
};
const FeedbackForm: React.FC<FeedbackFormProps> = ({editing, feedbackData}) => {
const router = useRouter();
const { register, handleSubmit } = useForm({
defaultValues: {
title: editing && feedbackData?.title ? feedbackData.title : '' ,
category: editing && feedbackData?.category ? feedbackData?.category : categories[0], // an array from local json file
...
}
})
/* you can use setState to save the feedbackData and use it for later */
/* because if it's not editing mode, you might have to refetch the data */
if (!feedbackData) return <div>{/* loading spinner etc. */}</div>;
return <div>{/* do something with data */}</div>;
};
export default FeedbackForm;
export const getServerSideProps = async (context: NextPageContext) => {
const { id } = context.query;
const feedbackData = await getFeedback(id as string);
return {
props: {
feedbackData: feedbackData,
},
};
};
TMI: I assume the editing mode is able to toggle, so it might be better to save data using useState even in the case of #2 too.
In conclusion, since the editing prop from the parent mainly decides the default value, I recommend using useEffect
export function React.useReducer<(state: PasswordResetFormState, action: PasswordResetAction) => PasswordResetFormState,PasswordResetFormState>reducer: (state: PasswordResetFormState, action: PasswordResetAction) => PasswordResetFormState, initializerArg: PasswordResetFormState, initializer: (arg: PasswordResetFormState) => ReducerStateWithoutAction<(state: PasswordResetFormState, action: PasswordResetAction) => PasswordResetFormState>): [ReducerStateWithoutAction<(state: PasswordResetFormState, action: PasswordResetAction) => PasswordResetFormState>, DispatchWithoutAction]
An alternative to useState.
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values. It also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
Version:
16.8.0
See also:
https://reactjs.org/docs/hooks-reference.html#usereducer
react
`useReducer(reducer: R, initialState: ReducerState , initializer?: undefined): [ReducerState , Dispatch >]`, `useReducer(reducer: R, initializerArg: ReducerStateWithoutAction , initializer?: undefined): [ReducerStateWithoutAction , DispatchWithoutAction]`, `useReducer(reducer: R, initializerArg: I & ReducerState , initializer: (arg: (I & ReducerState )) => ReducerState ): [ReducerState , Dispatch >]`, `useReducer(reducer: R, initializerArg: I, initializer: (arg: I) => ReducerState ): [ReducerState , Dispatch >]`
I am getting the above typescript error in my usereducer hook.
const [state, dispatch] = useReducer(
passwordResetReducer,
initialPasswordResetFormState
);
Invalid number of arguments, expected 3.
export const passwordResetReducer = (
state: PasswordResetFormState,
action: PasswordResetAction
): PasswordResetFormState => {
switch (action.case) {
default:
return { ...state, [action.field]: action.value };
}
};
export const initialPasswordResetFormState: PasswordResetFormState = {
email: "",
password1: "",
password2: "",
auth: "",
};
export interface PasswordResetFormState {
email: string;
password1: string;
password2: string;
auth: string;
}
export enum PasswordResetActionCase {
GENERAL = "GENERAL",
}
export type PasswordResetAction = {
case: PasswordResetActionCase.GENERAL;
value: string;
field: string;
};
I have spent ages on this, google returns nothing and I'm at a complete loss. If anyone can notice anything obvious in the code that should be changed I would hugely appreciate the help. I have multiple forms setup in the same way, and the error is common across every single one of them. I first noticed the errors when someone else in the same codebase shared their screen, they weren't showing up in my webstorm. After a fresh installation they came up for me, and I have spent the entire night trying to fix them to no avail.
State also shows as type never in the usereducer initialisation
solved - running npm install --save #types/react removes the errors
I am fetching data (an object) into a redux state and showing into textarea when user clicks on edit:
const regionData = useSelector((state) => state.myReducer.userDetailList.region);
but first time it gives me below error:
Uncaught TypeError: Cannot read properties of undefined (reading 'region')
also this error occurred when i changed code as per eslint suggestion:
i.e.
Use an object spread instead of `Object.assign` eg: `{ ...foo }`
Old Code:
return Object.assign({}, state, {
userDetailList: {
region: action.userDetailsPayload.region,
},
});
New Code:
const userDetailList = {
region: action.userDetailsPayload.region,
};
return { ...state, ...userDetailList };
As a result userDetailList showing as BLANK in Chrome redux-devtool. its working with old code (Object.assign)
I am new to react and redux, so any help is much appreciated. Thanks!
I had a promblem same this one.
Finally I've found out that if I use term "Reducer" or "State" at the end of state's name or reducer's name that are witten by me,
my code runs with error
Uncaught TypeError: Cannot read properties of undefined (reading 'result_201')
my reducer:
export function netInfoRecordReducer (
state = initialStateNetInfoRecord,
action : Actions
) : NetInfoRecordState {
switch (action.type) {
case SET_NET_INFO_RECORD: {
if ( action.payload.RESULT === "SUCCESS") {
return {
...state,
result_201: action.payload.RESULT_CONTENT,
};
}
return state;
}
default:
return state;
}
}
and my index.tsx file for defining CobineReducer and RoutState:
export default combineReducers({
netInfoRecordReducer: netInfoRecordReducer,
});
export interface RootState {
netInfoRecordState: NetInfoRecordState;
}
it runs with that error when I want acceess to the state in another file:
const netInfoRecord = useAppSelector( (state) =>
state.netInfoRecord.result_201);
console.log(netInfoRecord);
but if I remove "State" and "Reducer" term and change my index.tsx file to :
export default combineReducers({
netInfoRecord: netInfoRecordReducer,
});
export interface RootState {
netInfoRecord: NetInfoRecordState;
}
it works now ! :)
I'm doing some testing on my UI and I've noticed that if any state changes in my redux store my component (shown below) re-renders and restarts with embedded video at 0. If I type in a redux-connected text field, it remounts, if a status notification hits the store, it remounts, etc.
I have no idea how to fix this and I could really use some help figuring out how to go after the bug.
tldr; How can I stop my VideoPlayer from re-rendering each time something changes in my redux store?
redux-toolkit
react
component
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm);
// renders output here in the same component
return (
...
{sourceContentFound === false ? (
<VideoPlayerDisabled />
) : (
<VideoPlayerController
title={title}
description={description}
sourceAddress={iframeURL}
author={authorName}
timeStamp={{ startTime: 0 }}
/>
)}
)
...
}
formSlice
export const dynamicFormSlice = createSlice({
name: 'dynamicForm',
initialState,
reducers: {
updateField: (state, action) => {
state = action.payload;
return state;
}
},
});
export const selectDynamicForm = createSelector(
(state: RootState): dynamicFormState => state.dynamicForm,
dynamicForm => dynamicForm
);
statusHandlerSlice
I don't think this component does anything crazy, per-say, but I have a notification appear when the video conditions are met. When it goes back down clearStatus the video player restarts.
export const statusHandlerSlice = createSlice({
name: 'statusHandler',
initialState,
reducers: {
setStatus: (state, action: PayloadAction<IStatusObject>) => {
const { type, display, message } = action.payload;
state.status = {
...action.payload,
message: message.charAt(0).toUpperCase() + message.slice(1),
};
if (display === 'internal-only' || display === 'support-both') {
statusLogger(type, message);
}
},
clearStatus: state => {
state.status = {
type: 'success',
data: {},
message: '',
display: 'internal-only',
key: '',
};
},
},
});
export const { setStatus, clearStatus } = statusHandlerSlice.actions;
export const selectStatus = (state: RootState): IStatusObject =>
state.statusHandler.status;
Your MyComponent is re-render every time redux store state change is because you have a selector in it
You could stop this to happen by, add an equalityFn to useSelector.
You can write your own equalityFn or use some existing function from a library that supports deep comparison.
Ex: Use lodash isEqual
import { isEqual } from 'lodash';
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm, isEqual);
By default, useSelector use a shallow compare which can't detect deep changes inside your object, change to a deep comparison function like isEqual will help you to do that, but It's not recommended for all selector since there will be a performance impact.
Live Example:
I suggest either creating a custom equalFn to compare the data you're using in the current component or do not select the whole slice, maybe some properties change is unnecessary for your component. like:
const { data } = useSelector(store => store.sliceA, shallowEq);
// console.log(data) => { a: "foo", b: "bar" }
// data.b is useless but once it is changed, the component will re-render as well
return <Typography>{data.a}</Typography>
You should install React Devtools, turn on profiler, remember to check Record why each component rendered while profiling in setting to see what is causing re-rendering. sometimes custom hooks in libraries trigger re-rendering.
whyDidYouRender
is a good choice too
I am working with react hooks and typescript. I used useReducer() for global state. The action of the reducer function contains two properties name and data. name means the name of event or change and data will be particular data required for that particular name.
There are four value for name till now. If name "setUserData" then data should IUserData(interface). If name is setDialog then data should DialogNames(type containing two strings). And if its something else then data is not required.
//different names of dialog.
export type DialogNames = "RegisterFormDialog" | "LoginFormDialog" | "";
//type for name property in action object
type GlobalStateActionNames =
| "startLoading"
| "stopLoading"
| "setUserData"
| "setDialog";
//interface for main global state object.
export interface IGlobalState {
loading: boolean;
userData: IUserData;
dialog: DialogNames;
}
interface IUserData {
loggedIn: boolean;
name: string;
}
//The initial global state
export const initialGlobalState: IGlobalState = {
loading: false,
userData: { loggedIn: false, name: "" },
dialog: ""
};
//The reducer function which is used in `App` component.
export const GlobalStateReducer = (
state: IGlobalState,
{ name, data }: IGlobalStateAction
): IGlobalState => {
switch (name) {
case "startLoading":
return { ...state, loading: true };
case "stopLoading":
return { ...state, loading: false };
case "setUserData":
return { ...state, userData: { ...state.userData, ...data } };
case "setDialog":
return { ...state, dialog: data };
default:
return state;
}
};
//The interface object which is passed from GlobalContext.Provider as "value"
export interface GlobalContextState {
globalState: IGlobalState;
dispatchGlobal: React.Dispatch<IGlobalStateAction<GlobalStateActionNames>>;
}
//intital state which is passed to `createContext`
export const initialGlobalContextState: GlobalContextState = {
globalState: initialGlobalState,
dispatchGlobal: function(){}
};
//The main function which set the type of data based on the generic type passed.
export interface IGlobalStateAction<
N extends GlobalStateActionNames = GlobalStateActionNames
> {
data?: N extends "setUserData"
? IUserData
: N extends "setDialog"
? DialogNames
: any;
name: N;
}
export const GlobalContext = React.createContext(initialGlobalContextState);
My <App> component looks like.
const App: React.SFC = () => {
const [globalState, dispatch] = React.useReducer(
GlobalStateReducer,
initialGlobalState
);
return (
<GlobalContext.Provider
value={{
globalState,
dispatchGlobal: dispatch
}}
>
<Child></Child>
</GlobalContext.Provider>
);
};
The above approach is fine. I have to use it like below in <Child>
dispatchGlobal({
name: "setUserData",
data: { loggedIn: false }
} as IGlobalStateAction<"setUserData">);
The problem is above approach is that it makes code a little longer. And second problem is I have to import IGlobalStateAction for not reason where ever I have to use dispatchGlobal
Is there a way that I could only tell name and data is automatically assigned to correct type or any other better way. Kindly guide to to the correct path.
Using useReducer with typescript is a bit tricky, because as you've mentioned the parameters for reducer vary depending on which action you take.
I came up with a pattern where you use classes to implement your actions. This allows you to pass typesafe parameters into the class' constructor and still use the class' superclass as the type for the reducer's parameter. Sounds probably more complicated than it is, here's an example:
interface Action<StateType> {
execute(state: StateType): StateType;
}
// Your global state
type MyState = {
loading: boolean;
message: string;
};
class SetLoadingAction implements Action<MyState> {
// this is where you define the parameter types of the action
constructor(private loading: boolean) {}
execute(currentState: MyState) {
return {
...currentState,
// this is how you use the parameters
loading: this.loading
};
}
}
Because the state update logic is now encapsulated into the class' execute method, the reducer is now only this small:
const myStateReducer = (state: MyState, action: Action<MyState>) => action.execute(state);
A component using this reducer might look like this:
const Test: FunctionComponent = () => {
const [state, dispatch] = useReducer(myStateReducer, initialState);
return (
<div>
Loading: {state.loading}
<button onClick={() => dispatch(new SetLoadingAction(true))}>Set Loading to true</button>
<button onClick={() => dispatch(new SetLoadingAction(false))}>Set Loading to false</button>
</div>
);
}
If you use this pattern your actions encapsulate the state update logic in their execute method, which (in my opinion) scales better, as you don't get a reducer with a huge switch-case. You are also completely typesafe as the input parameter's types are defined by the action's constructor and the reducer can simply take any implementation of the Action interface.