Callback set in the state variable of Context Provider will be undefined - javascript

I have referred other questions asked here by OPs but none seems to work for me. I have one layout and in that layout there is one toolbar which I am using to submit forms. Now to make that happen I using one FormProvider which is a Context.Provider (wraps layout component) with a state variable which stores function callback to submit a form. Now in the form component (which is loaded conditionally) I am using this setState func from context to assign form submit callback and in the toolbar using state variable from context to call that as a function. The problem I am facing is I always get state variable from context undefined. These are the snippets from my code.
FormProvider
type FormContextProps = {
setOnSubmit: (callable: Function | undefined) => void
assignOnSubmit: Dispatch<any>
setOnCancel: (callable: Function | undefined) => void
submit: (e: any) => void
cancel: () => void
}
const initAuthContextPropsState = {
setOnSubmit: () => { },
assignOnSubmit: () => { },
setOnCancel: () => { },
submit: (e: any) => { },
cancel: () => { },
}
const FormContext = createContext<FormContextProps>(initAuthContextPropsState)
const useTForm = () => {
return useContext(FormContext)
}
const FormProvider: FC = ({ children }) => {
const [onSubmit, assignOnSubmit] = useState<Function | undefined>()
const [onCancel, assignOnCancel] = useState<Function | undefined>()
const setOnSubmit = (callable: Function | undefined) => {
console.log('setOnSubmit', callable)
assignOnSubmit(callable)
console.log('setOnSubmit after', onSubmit)
}
const setOnCancel = (callable: Function | undefined) => {
assignOnCancel(callable)
}
useEffect(() => {
console.log("changed onSubmit"); // this hook is called only on first render
}, [onSubmit])
const submit = (e: any) => {
console.log('form submited', onSubmit) // this is always undefined when I click on save button on toolbar
if (onSubmit) onSubmit(e)
}
const cancel = () => {
if (onCancel) onCancel()
}
return (
<FormContext.Provider value={{ setOnSubmit, assignOnSubmit, setOnCancel, submit, cancel }}>
{children}
</FormContext.Provider>
)
}
Toolbar
const FormToolbar: FC = () => {
const {classes} = useLayout()
const {submit, cancel} = useTForm()
const submitForm = (e: any) => submit(e)
return (
<div className='toolbar' id='kt_toolbar'>
{/* begin::Container */}
<div
id='kt_toolbar_container'
className={clsx(classes.toolbarContainer.join(' '), 'd-flex flex-stack')}
>
<DefaultTitle />
{/* begin::Actions */}
<div className='py-1 d-flex align-items-center'>
{/* begin::Button */}
<button
className='btn btn-sm btn-primary me-4'
onClick={submitForm}
>
Save
</button>
<button
className='btn btn-sm btn-primary'
onClick={cancel}
>
Cancel
</button>
{/* end::Button */}
</div>
{/* end::Actions */}
</div>
{/* end::Container */}
</div>
)
}
EditForm.tsx
const EditForm: React.FC<Props> = () = {
const form = useRef() as React.MutableRefObject<HTMLFormElement>
const { setOnSubmit, assignOnSubmit } = useTForm()
useLayoutEffect(() => {
setOnSubmit(() => { form?.current.dispatchEvent(new Event('submit')) });
console.log('Form changed', form)
}, [form])
return (
<form onSubmit={formik.handleSubmit} ref={form}>
// ...
</form>
);
}
Main Component
function StaffManagement({ user, selectedLanguageId, idMenu }: Props) {
const [editing, setEditing]: [any, Function] = useState(null)
return (
<div className='row'>
<div className="col-lg-4">
<ModuleItemList
className='card-xxl-stretch mb-xl-3'
edit={setEditing}
/>
</div>
<div className="col-lg-8">
{editing && <EditForm
userId={user.id}
menuId={idMenu}
/>}
</div>
</div>
)
}

When using setState, we can do it in two ways,
setState(newState); // directly pass the new state
setState((currentState) => newState); // return the new state from a callback fn
That mean setState can accept a plain state value or a callback function which will return the new state that need to be set as the new state.
When you say,
const setOnSubmit = (callable: Function | undefined) => {
// callable => () => { form?.current.dispatchEvent(new Event('submit')) }
assignOnSubmit(callable)
}
Here React thinks you used the setState(assignOnSubmit) in the 2nd way I mentioned above, so react will call your callback and execute the form?.current.dispatchEvent(new Event('submit')). Since your callable function returns nothing, undefined will assigned to your onSubmit state.
So if you really need to store this function in a state, you have to do it as,
const setOnSubmit = (callable: Function | undefined) => {
assignOnSubmit(() => callable) // React will call the callback and return callable
}
Few Other Tips
Also do not use useLayoutEffect for this task. You can use useEffect and imrpove the performance of your application.
memoize the provider data, otherwise you will trigger unwanted re renders.
const data = useMemo(() => ({
setOnSubmit, assignOnSubmit, setOnCancel, submit, cancel
}), [submit, cancel, setOnCancel, assignOnSubmit, setOnSubmit])
return (
<FormContext.Provider value={data}>
{children}
</FormContext.Provider>
)
State updates are asynchronous. So you can't expect to console.log log the latest state
const setOnSubmit = (callable: Function | undefined) => {
console.log('setOnSubmit', callable)
assignOnSubmit(() => callable)
console.log('setOnSubmit after', onSubmit) // this won't log the latest as assignOnSubmit is async
}

you need to make <FormProvider as a parent of your Toolbar and EditForm to make your context working properly. Based on your code I don't see where you put the <FormProvider, so I'm guessing that you need to put it on your Main Component
Main Component
function StaffManagement({ user, selectedLanguageId, idMenu }: Props) {
const [editing, setEditing]: [any, Function] = useState(null)
return (
<FormProvider>
<div className='row'>
<div className="col-lg-4">
<ModuleItemList
className='card-xxl-stretch mb-xl-3'
edit={setEditing}
/>
</div>
<div className="col-lg-8">
{editing && <EditForm
userId={user.id}
menuId={idMenu}
/>}
</div>
</div>
</FormProvider>
)

Related

React calling component function outside of the component

I have a functional component:
function Tile(props: any) {
const selectTile = (() => {
...
})
return (
<div>
...
</div>
)
}
I then create another functional component which will hold many tiles.
function Grid(props: any) {
const selectAllTiles = (() => {
})
return (
<div>
<Tile></Tile>
<Tile></Tile>
<Tile></Tile>
</div>
)
}
My question is, how can I call the selectTile() function for each Tile within the selectAllTiles() function? What approaches are there?
I would make the parent component keep track of selected tiles, and it will then inform each Tile (through props) if it is selected or not.
If a Tile should be able to "make itself selected", then a function can be passed from the parent into each Tile (also through props) that the Tile can call and which will update the selected tiles collection in the parent.
Updated code:
function Tile(props: any) {
const { isSelected, makeSelected } = props;
return (
<div style={{ ... something based on isSelected ... }}>
<button type="button" onClick={makeSelected}>Select me</button>
</div>
)
}
function Grid(props: any) {
const [isSelected, setIsSelected] = useState<bool[]>([false, false, false]);
const makeSelected = (index: int) => {
// ...
setIsSelected(...);
}
return (
<div>
<Tile isSelected={isSelected[0]} makeSelected={() => makeSelected(0)}></Tile>
<Tile isSelected={isSelected[1]} makeSelected={() => makeSelected(1)}></Tile>
<Tile isSelected={isSelected[2]} makeSelected={() => makeSelected(2)}></Tile>
</div>
)
}
This is just an (untested) example, many approaches are possible.
why don't use like this:
function Tile(props: any) {
return (
<div>
...
</div>
)
}
const selectTile = (() => {
...
})

React update view after submitting form data using react-query in NextJS

I'm working on a NextJS project, and am running into an issue where my view isn't updating after a form submission. I'm using react-query to both fetch the data and do mutations.
I have a form called 'Add Day' which adds a day object to the database through an endpoint. But after making this update, my UI does not refresh to reflect this new object.
Here is a video that demonstrates it:
https://imgur.com/a/KIcQOfs
As you can see, when I refresh the page, the view updates. But I want it to update as soon as I submit the form and it is a success. Here's how my code is set up:
TripDetail
const TripDetailPage = () => {
const router = useRouter()
const queryClient = useQueryClient()
const tripId = router.query.tripId as string
const { data, isError, isLoading } = useQuery({
queryKey: ['trips', tripId],
queryFn: () => getTrip(tripId),
})
const [tabState, setTabState] = useState(1)
const toggleTab = (index: number) => {
setTabState(index)
}
const [modalVisible, setModalVisible] = useState<boolean>(false)
const toggleModalVisible = () => {
setModalVisible(!modalVisible)
}
const onDayAdded = () => {
queryClient.invalidateQueries({ queryKey: ['trips', tripId] })
console.log('refetching')
console.log('day was added')
}
console.log('new data?')
console.log(data)
if (isLoading) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error</span>
}
if (!data || !data.data || !data.data.name || !data.data.days) {
return <span>Error</span>
}
return (
<div className="ml-12 mr-12 mt-6">
<div className="flex flex-row justify-between">
<div className="order-first flex items-center">
<h1 className="text-2xl font-semibold text-slate-800">{data.data.name}</h1>
<Button className="btn btn-primary ml-8">Invite</Button>
</div>
<div className="order-last flex">
<Button className="btn btn-primary mr-4">Refresh</Button>
<Button className="btn btn-primary" onClick={toggleModalVisible}>
Add Day
</Button>
</div>
<AddDayModal
onSuccess={onDayAdded}
modalVisible={modalVisible}
toggleModalVisible={toggleModalVisible}
trip={data.data}
/>
</div>
<div id="tabs-nav" className="font-medium text-lg border-separate pb-2 border-b-2 border-gray-200 mt-6">
<TabButton tabId={1} currentTabState={tabState} name="Plans" toggleTab={() => toggleTab(1)}></TabButton>
<TabButton
tabId={2}
currentTabState={tabState}
name="Calendar"
toggleTab={() => toggleTab(2)}
></TabButton>
<TabButton tabId={3} currentTabState={tabState} name="Ideas" toggleTab={() => toggleTab(3)}></TabButton>
</div>
<div id="content">
{tabState === 1 && <PlannerBoard days={data.data.days} tripId={tripId}></PlannerBoard>}
{tabState === 2 && <h1>Tab 2</h1>}
{tabState === 3 && <h1>Tab 3</h1>}
</div>
</div>
)
}
export default TripDetailPage
I'm calling invalidateQueries in onDayAdded which should refetch my data and update my view, but it doesn't seem to be. PlannerBoard is where I pass the data and is the component that renders the columns. It basically iterates through the days that is passed to it and renders them.
Planner Board:
type TDndPlannerProps = {
days: TDay[]
tripId: string
}
const PlannerBoard = (props: TDndPlannerProps) => {
// ...
props.days.forEach((day) => {
// lots of processing, creates a data object which has the objects to make this view
})
const [state, setState] = useState<IData>(data) // after above processing
// removed a bunch of code
return (
<div className="overflow-auto">
<div>
{day.map((day) => {
return <DayColumn key={day.id} day={day} />
})}
</div>
</div>
)
}
export default PlannerBoard
AddDay Modal:
type TModalProps = {
trip: TTrip
modalVisible: boolean
toggleModalVisible: () => void
onSuccess: () => void
}
const formArr: TFormField[] = [
{
label: 'Day name',
name: 'name',
type: 'text',
},
{
label: 'Day date',
name: 'date',
type: 'date',
},
]
const AddDayModal = (props: TModalProps) => {
const [day, setDay] = useState<TDay>({
name: '',
tripId: props.trip.id!,
})
const onSuccess = () => {
props.toggleModalVisible()
props.onSuccess()
}
const { mutate, isLoading, isError, error } = useMutation((day: TDay) => postDay(day), { onSuccess })
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
mutate(day)
}
const onUpdate = (update: TFormFieldValue) => {
setDay((prev) => {
if (update.type == 'date') {
return { ...prev, [update.name]: new Date(update.value) }
} else {
return { ...prev, [update.name]: update.value }
}
})
}
const formModalProps: TFormModalProps = {
title: 'Add Day',
formArr,
submitBtn: 'Submit',
submittingBtn: 'Adding',
toggleVisible: props.toggleModalVisible,
visible: props.modalVisible,
onSubmit,
isLoading,
isError,
error,
onUpdate,
}
return (
<div>
<FormModal {...formModalProps} />
</div>
)
}
export default AddDayModal
The mutate here makes the axios call to my API which updates the db with this form data. Once we hit onSuccess the API call has succeeded and it call's TripDetailPage's onDayAdded().
Any idea how to make my UI update when the refetch completes? Do I need to use useState somehow?
I believe this is the expected behaviour with react-query.
You can either use queryClient.invalidateQueries to refetch single or multiple queries in the cache or use queryClient.setQueryData to immediately update a query's cached data.
You can use this directly in the onSuccess callback if you wish.
See documentation.
I believe I have a fix. In my PlannerBoard I needed to detect the change in props through useEffect and then manually set the state again as:
useEffect(() => {
const data: IData = createBoardData(props.days)
setState(data)
}, [props])
This forces the view to re-render.
There is likely no need to copy data from props into state in the PlannerBoard component. As you have found out, this is your issue, because you create a "copy" of your state. This is usually an anti-pattern, and the fix you found (syncing with useEffect) is not a good idea either. I have a whole blogpost series around that topic:
https://tkdodo.eu/blog/dont-over-use-state
https://tkdodo.eu/blog/putting-props-to-use-state
the easiest fix would be to move from useState to useMemo, unless you really need the setState method, which you haven't shown in your example:
const PlannerBoard = (props: TDndPlannerProps) => {
const state = React.useMemo(() => {
// lots of processing
}, [props.days])

How to pass callback function parameters to parent react component?

Just wondering the best way to pass the letterSelected into the useLazyQuery fetchMovies query, so that I don't have to use the static variable of "A". I was hoping there was a way to pass it directly into fetchMovies. useLazyQuery is an apollo query.
const BrowseMovies = () => {
const [fetchMovies, { data, loading}] = useLazyQuery(BROWSE_MOVIES_BY_LETTER, {
variables: {
firstLetter: "A"
}
})
return (
<div className="browserWrapper">
<h2>Browse Movies</h2>
<AlphabetSelect
pushLetterToParent={fetchMovies}
/>
{
data && !loading &&
data.browseMovies.map((movie: any) => {
return (
<div className="browseRow">
<a className="movieTitle">
{movie.name}
</a>
</div>
)
})
}
</div>
)
}
export default BrowseMovies
const AlphabetSelect = ({pushLetterToParent}: any) => {
const letters = ['A','B','C','D','E','F','G','H','I','J','K','L', 'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','#']
const [selectedLetter, setSelectedLetter] = useState("A")
const onLetterSelect = (letter: string) => {
setSelectedLetter(letter.toUpperCase())
pushLetterToParent(letter.toUpperCase())
}
return (
<div className="alphabetSelect">
{
letters.map((letter: string) => {
return(
<div
className={selectedLetter === letter ? 'letterSelectActive' : 'letterSelect'}
onClick={() => onLetterSelect(letter)}
>
{letter}
</div>
)
})
}
</div>
)
}
export default AlphabetSelect
This appears to be a problem solved by Lifting State Up. useLazyQuery takes a gql query and options and returns a function to execute the query at a later time. Sounds like you want the child component to update the variables config parameter.
BrowseMovies
Move firstLetter state BrowseMovies component
Update query parameters/options/config from state
Add useEffect to trigger fetch when state updates
Pass firstLetter state and setFirstLetter state updater to child component
const BrowseMovies = () => {
const [firstLetter, setFirstLetter] = useState('');
const [fetchMovies, { data, loading}] = useLazyQuery(
BROWSE_MOVIES_BY_LETTER,
{ variables: { firstLetter } } // <-- pass firstLetter state
);
useEffect(() => {
if (firstLetter) {
fetchMovies(); // <-- invoke fetch on state update
}
}, [firstLetter]);
return (
<div className="browserWrapper">
<h2>Browse Movies</h2>
<AlphabetSelect
pushLetterToParent={setFirstLetter} // <-- pass state updater
selectedLetter={firstLetter} // <-- pass state
/>
{
data && !loading &&
data.browseMovies.map((movie: any, index: number) => {
return (
<div key={index} className="browseRow">
<a className="movieTitle">
{movie.name}
</a>
</div>
)
})
}
</div>
)
}
AlphabetSelect
Attach pushLetterToParent callback to div's onClick handler
const AlphabetSelect = ({ pushLetterToParent, selectedLetter }: any) => {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#';
return (
<div className="alphabetSelect">
{
letters.split('').map((letter: string) => {
return(
<div
key={letter}
className={selectedLetter === letter ? 'letterSelectActive' : 'letterSelect'}
onClick={() => pushLetterToParent(letter.toUpperCase())}
>
{letter}
</div>
)
})
}
</div>
)
}

React, handle modal from component

How can I catch the click on some buttom from a modal, to return true or false to the component that is calling the modal?
handleSubmitSaveConfigurations = async (row) => {
const { scadaValidation } = this.props;
const result = await scadaValidation(11);
if (result.statusCode !== 200) {
// Opens the modal to ask if you really want to save
this.setState({openSimpleModal: true});
this.setState({contentSimpleModal: this.warningModal()});
// Here I have to catch if the modal click yes or no.
// In case yes, do nothing and continue with the code
// But in case "no" returns false and stops
}
// If result.statusCode === 200 returns true
return true;
}
warningModal = () => (
<div>
Do you want to save?
<Button id="btnClose" onClick={() => this.handleModalClickClose()}>No</Button>
<Button id="btnSave" onClick={() => this.handleModalClickClose()}>Yes</Button>
</div>
);
handleModalClickClose = () => this.setState({ openSimpleModal: false });
You could pass a handler to be executed inside your modal.
const Modal = ({ callback }) =>{
const handleClick = arg => callback(arg)
return(
<div>
<button onClick={() => handleClick('button1')}>A</button>
<button onClick={() => handleClick('button2')}> B</button>
</div>
)
}
And expect to receive this value inside the component which is calling Modal
const TheOneWhoCalls = () =>{
const onModalClick = arg => console.log(arg)
return <Modal callback={onModalClick} />
}
You can create a function on the parent component, and inside the modal, u only use it.
https://reactjs.org/docs/lifting-state-up.html#lifting-state-up
Parent:
constructor () {
this.state: {test: false}
}
setStateTest (value) {
this.setState(value)
}
render () {
return <modal handlerSetParentStateTest = {setStateTest}></modal>
}
Modal:
// this will set the parent state
this.props.handlerSetParentStateTest(true);
I want to share my solution, for sure I will need it in the future. it the implementation of #Dupocas
const Modal = ({ callback }) => {
const handleClick = arg => callback(arg)
return (
<div>
Wanna save?
<Button id="btnCloseModal" onClick={() => handleClick(0)}>No</Button>
<Button id="btnGuardarConfirm" onClick={() => handleClick(1)}>Sí</Button>
</div>)
};
class TableDisplayReportRecord extends Component<Props, State> {
constructor {...}
handleValidate = async (row) => {
const { scadaValidation } = this.props;
const verify = await scadaValidation();
if (verify.statusCode !== 200) {
this.setState({openSimpleModal: true});
const onModalClick = arg => {
this.setState({openSimpleModal: false});
//do nothing
if (arg === 0) return false;
//if user selected "Yes", call the function that I need
else this.handleSubmitSave(row);
};
this.setState({contentSimpleModal:
<Modal
callback={onModalClick}
/>
})
}
}
handleSubmitSave = async (row) => {...}
...
}

Using setState with onChange does not update

What am I looking for?
A way to use setState() on an input element that updates my state immediately with onChange.
What have I done so far?
I have two components, BooksApp and SearchBook. I am passing result to SearchBook as a prop to be rendered. Then I am capturing the changes on the input element using onChange. The onChange is calling the method I passed as a prop as well.
React documentation says the following, but if I use a callback and then in the callback I invoke setState() it sounds to me that I am back at the same situation, because setState() will again be async:
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
What is the issue?
If the user erases the content of the input too quickly, although there is still a console.log('empty') in the console, the setState() does not run and thus result stays the same and does not become an empty array.
This is the content of App.js:
class BooksApp extends Component {
state = {
books: [],
result: [],
query: ''
}
updateQuery = (query) => {
this.setState({ query: query.trim() })
}
objDigger = (arr, diggers) => {
const digged = [];
diggers.forEach((d) => {
let o = {};
o = arr.filter((a) => a.id === d);
o.forEach((i) => {
digged.push(i);
})
});
return digged;
}
filterHelper = (result) => {
const resultKeys = result.map((r) => r.id)
const stateKeys = this.state.books.map((s) => s.id)
// this.objDigger(this.state.books, stateKeys)
// new books
const newKeys = _array.difference(resultKeys, stateKeys);
const newObjs = this.objDigger(result, newKeys)
// existing books
const existingKeys = _array.difference(resultKeys, newKeys)
const existingObjs = this.objDigger(this.state.books, existingKeys);
// search books
const searchObjs = newObjs.concat(existingObjs);
return searchObjs;
}
searchBook = (query) => {
this.updateQuery(query);
if (query) {
BooksAPI.search(query).then((result) => {
result = this.filterHelper(result)
if (!result.error) {
this.setState({
result,
})
}
})
} else {
console.log('empty');
this.setState(state => ({
result: []
}));
}
}
appHandleChange = (query) => {
console.log('searching');
this.searchBook(query);
}
render() {
return (
<div className="app">
<Route path="/search" render={() => (
<SearchBook
result={ this.state.result }
onChange={ this.appHandleChange }
onChangeShelf={ this.changeShelf }/>
)}>
</Route>
</div>
)
}
}
This is the content of SearchBook:
class SearchBook extends Component {
handleChange = (book, newShelf) => {
this.props.onChangeShelf(book, newShelf);
}
handleInputChange = (event) => {
this.props.onChange(event.target.value);
}
render() {
const { result } = this.props;
return (
<div className="search-books">
<div className="search-books-bar">
<Link
to="/"
className="close-search"
>
Close
</Link>
<div className="search-books-input-wrapper">
<input
type="text"
placeholder="Search by title or author"
onChange={ (event) => this.handleInputChange(event) }
/>
</div>
</div>
<div className="search-books-results">
<ol className="books-grid">
{result.length>0 && result.map((book) => (
<li key={book.id}>
<div className="book">
<div className="book-top">
<div className="book-cover" style={{ width: 128, height: 193, backgroundImage: `url(${book.imageLinks && book.imageLinks.smallThumbnail})` }}></div>
<BookShelfChanger
shelf={book.shelf ? book.shelf : 'none'}
onChangeShelf={(newShelf) => {
this.handleChange(book, newShelf)
}}
></BookShelfChanger>
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
</li>
))}
</ol>
</div>
</div>
)
}
}

Categories

Resources