What is the best way to set react-hook-form defaultValues in this case? - javascript

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

Related

How to indicate to TS that value in store has value even though it's optional?

I have the following case:
I have a standard store with optional items in it.
I also have a tree of elements which rely on that store. I also select {account} in multiple components.
For business logic, I had to check at the very top if account is set. If it is not, I don't render the components which rely on it.
How can I tell TS that even though the value is optional in store I'm 100% sure it is NOT undefined?
Example code:
// store
interface Account {
id: number;
name: string;
}
export interface AppState {
account?: Account;
}
const initialState: AppState = {};
const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
setAccount(state: AppState, action: PayloadAction<Account | undefined>) {
state.account = action.payload;
}
}
});
// component
const GrandChild = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild /> : <>account not set</>;
};
export default function App() {
const dispatch = useDispatch();
useEffect(() => {
// dispatch(setAccount({ id: 0, name: "john" }));
});
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Child />
</div>
);
}
Codesandbox:
https://codesandbox.io/s/required-reducer-ysts49?file=/src/App.tsx
I know I can do this:
const { account } = useSelector((state: RootState) => state, shallowEqual) as Account;
but this seems very hacky. Is there a better way?
Well you know that Grandchild wont be rendered if account is undefined. so why not pass account as a property to the Grandchild component. This way you could define the property as never being undefined. And since you only render Grandchild after you checked that account isn't undefined you should be able to pass account as the property to the component (and since you defined the property as not being undefined TS will not object to account.name in your Grandchild component.
I don't know redux however - I have never used it and don't know anything about it, so I don't know if this answer is compatible with that or if redux will cause some issues I couldn't forsee.
I've written a little bit of code of how this could look (but as I already said, I don't know how to use redux, so you'll probably have to take my idea and write it so everything works) - so my code example is probably more of a visualization of what I mean than a solution.
const GrandChild = (account: Account) => {
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild account={account} /> : <>account not set</>;
};

Any change to redux store my causes component to re-render

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

React hook, wired issue when use useState, while if use setState work perfectly, how to solve it

dear community, I am facing a wired issue, and I don't know how to summary my situation in the question title, so I wonder if the question title is accurate enough.
I was trying to convert a class component to a hook component.
The class version code like this
async componentDidMount() {
const { dispatch, itemId } = this.props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment', //here to fetch submissions in props
payload: {
id: itemId
}
});
}
const { submissions } = this.props;
this.setState({
studentSubmissions: submissions,
});
} catch (error) {
throw error.message;
}
}
render() {
const { studentSubmissions } = this.state;
return (
<Table dataSource={studentSubmissions} />
)
}
export default SubmissionsDetail;
and in hook, it look like this
const [studentSubmissions, setStudentSubmissions] = useState([]);
useEffect(() => {
async function fetchSubmissions() {
const { dispatch, itemId } = props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment',
payload: {
id: itemId
}
});
}
const { submissions } = props;
setStudentSubmissions(submissions)
} catch (error) {
throw error.message;
}
};
fetchSubmissions()
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
I omitted some code for better reading, like connect to redux store or others.
and the component is import in the parent file like this
import SubmissionsDetail from './SubmissionsDetail'
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId}/>
})}
it work perfect in class component, the expected result should return tables like this
However, when I change to use hook, the result return like this
or sometimes all data in tables become submissions3
I try to console.log(submissions) inside the try{...} block, when in class, the result is
which is correct, there have two assignments, the one have 4 submissions, another one have zero submission.
But the output in hook is different, the result is like this
either both have 4 submissions, either both have zero. That means one obj affect all other obj.
It seems like if useState change, it would influence other objs, that make me really confused. I think in the map method, each item is independent, right? If so, and how to explain why it work perfectly in class setState, but failed in hook useState?
I hope my question is clear enough, If you know how to describe my question in short, plz let me know, I would update the title, to help locate experts to answer.
Please don't hesitate to share your opinions, I really appreciate and need your help, many thanks!
Edit: You are probably going to want to rework the way you store the submission inside of the redux store if you really want to use the Hook Component. It seems like right now, submissions is just an array that gets overwritten whenever a new API call is made, and for some reason, the Class Component doesn't update (and it's suppose to update).
Sorry it's hard to make suggestions, your setup looks very different than the Redux environments I used. But here's how I would store the submissions:
// no submissions loaded
submissions: {}
// loading new submission into a state
state: {
...state,
sessions: {
...state.session,
[itemId]: data
}
}
// Setting the state inside the component
setStudentSubmissions(props.submissions[itemId])
And I think you will want to change
yield put({
type: 'getSubmissions',
payload: response.data.collections
});
to something like
yield put({
type: 'getSubmissions',
payload: {
data: response.data.collections,
itemId: id
});
If you want to try a "hack" you can maybe get a useMemo to avoid updating? But again, you're doing something React is not suppose to do and this might not work:
// remove the useEffect and useState, and import useMemo
const studentSubmissions = useMemo(async () => {
try {
if (itemId) {
await dispatch({
type: "assignment/fetchSubmissionsByAssignment", //here to fetch submissions in props
payload: {
id: itemId,
},
});
return this.props.submissions;
}
return this.props.submissions;
} catch (error) {
throw error.message;
}
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
There is no reason to use a local component state in either the class or the function component versions. All that the local state is doing is copying the value of this.props.submissions which came from Redux. There's a whole section in the React docs about why copying props to state is bad. To summarize, it's bad because you get stale, outdated values.
Ironically, those stale values were allowing it to "work" before by covering up problems in your reducer. Your reducer is resetting the value of state.submissions every time you change the itemId, but your components are holding on to an old value (which I suspect is actually the value for the previous component? componentDidMount will not reflect a change in props).
You want your components to select a current value from Redux based on their itemId, so your reducer needs to store the submissions for every itemId separately. #Michael Hoobler's answer is correct in how to do this.
There's no problem if you want to keep using redux-saga and keep using connect but I wanted to give you a complete code so I am doing it my way which is with redux-toolkit, thunks, and react-redux hooks. The component code becomes very simple.
Component:
import React, { useEffect } from "react";
import { fetchSubmissionsByAssignment } from "../store/slice";
import { useSelector, useDispatch } from "../store";
const SubmissionsDetail = ({ itemId }) => {
const dispatch = useDispatch();
const submissions = useSelector(
(state) => state.assignment.submissionsByItem[itemId]
);
useEffect(() => {
dispatch(fetchSubmissionsByAssignment(itemId));
}, [dispatch, itemId]);
return submissions === undefined ? (
<div>Loading</div>
) : (
<div>
<div>Assignment {itemId}</div>
<div>Submissions {submissions.length}</div>
</div>
);
};
export default SubmissionsDetail;
Actions / Reducer:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchSubmissionsByAssignment = createAsyncThunk(
"assignment/fetchSubmissionsByAssignment",
async (id) => {
const response = await getSubmissionsByAssignment(id);
// can you handle this in getSubmissionsByAssignment instead?
if (response.status !== 200) {
throw new Error("invalid response");
}
return {
itemId: id,
submissions: response.data.collections
};
}
);
const initialState = {
submissionsByItem: {}
};
export default createReducer(initialState, (builder) =>
builder.addCase(fetchSubmissionsByAssignment.fulfilled, (state, action) => {
const { itemId, submissions } = action.payload;
state.submissionsByItem[itemId] = submissions;
})
// could also respond to pending and rejected actions
);
if you have an object as state, and want to merge a key to the previous state - do it like this
const [myState, setMyState] = useState({key1: 'a', key2: 'b'});
setMyState(prev => {...prev, key2: 'c'});
the setter of the state hook accepts a callback that must return new state, and this callback recieves the previous state as a parameter.
Since you did not include large part of the codes, and I assume everything works in class component (including your actions and reducers). I'm just making a guess that it may be due to the omission of key.
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId} key={itemId} />
})}
OR it can be due to the other parts of our codes which were omitted.

NGRX entity updateOne not working: id undefined

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!

How would I pass a getParam param to another screen using react-navigation in a stateless component to fire up a graphql mutation?

Following the docs: react-navigation params, I'd basically do something like this (parent):
this.props.navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
And then passing to the child
class DetailsScreen extends React.Component {
render() {
const { navigation } = this.props;
const itemId = navigation.getParam('itemId', 'NO-ID');
const otherParam = navigation.getParam('otherParam', 'some default value');
return ( (..omited for brevity)
EDIT:
My situation is that in one screen/container we fire up a graphql mutation that takes a phoneNumber and retrieves a code to user.
Onto the next screen I need to take this phoneNumber that the user has just inserted to fire another mutation that takes phoneNumber and code to make the authentication. I don't want to use states to do that, since there's api available on react-navigation.
Parent:
signInUser(phoneNumber: string) {
const { navigation, sendCode } = this.props
sendCode({ variables: { phoneNumber } }) // graphql mutation
navigation.navigate('Authenticate', { phoneNumber }) // passing
// phoneNumber as param
}
Child:
export const AuthenticationCodeModal = (props: NavigationScreenProps) => {
const {navigation} = props
const phoneNumber = navigation.getParam(phoneNumber)
// console.log(phoneNumber)
// I need to get phoneNumber here and pass it as props to
// <AuthenticationFormContainer> which will fire up auth mutation
return(
<View style={styles.container} >
<AuthenticationFormContainer/>
</View>
)
}
It better and interesting if you use redux for task like this. Redux has a global state which can be assessed by any component
Turns out it was a simple syntax error of the value that getParam receives, doing this fixed my problem:
navigation.navigate('Authenticate', { phoneNumber: {phoneNumber: phoneNumber} })

Categories

Resources