I am trying to access useNavigate hooke inside createAsyncThunk in redux toolkit as shown below:
export const onHandleBookmark = createAsyncThunk('articles/example', async (id: string, { dispatch, getState }) => {
const navigate = useNavigate()
console.log('DDD')
console.log('LLL')
//rest of the processing
})
Unfortunately no logs are getting displayed as it exhausts on first line of execution. Can someone tell me what's going wrong?
Option A (simpler, needs redux-toolkit):
When using createAsyncThunk api, the returned function is promisified by default. You can wait for the promise resolution using unwrap() method (inpired by Rust, I guess…). When the promise is resolved, it can be further chained with then() calls, so you can navigate from there:
import { useSelector, useDispatch } from "react-redux"
import { useNavigate } from "react-router"
import { logout } from "../store/auth"
const LogoutButton = () => {
const dispatch = useDispatch()
const navigate = useNavigate()
return (
<button
onClick={() =>
dispatch(logout())
.unwrap() // <-- async Thunk returns a promise, that can be 'unwrapped')
.then(() => navigate("/"))
}
>
Log out
</button>
)
}
Option B (more complected, but works in vanilla react-redux):
While you cannot call useNavigate() outside the React component, you can pass it as an argument to a dispatch() call:
Example component:
import { useSelector, useDispatch } from "react-redux"
import { useNavigate } from "react-router"
import { logout } from "../store/auth"
const LogoutButton = () => {
const dispatch = useDispatch()
const navigate = useNavigate() // <-- Grab the 'navigate' reference
return (
<button
onClick={() => dispatch(logout(navigate))} // <-- Pass 'navigate' as argument
>
Log out
</button>
)
}
Redux Async Thunk
Then you can call the function reference passed as argument in the thunk:
export const logout = createAsyncThunk(
"[auth] logout",
async (navigate) => {
const response = await axios.get("auth/logout")
if (response) {
navigate("/")
return response
}
})
Calling a hook like useNavigate outside of a component is not allowed - and in this case, it will probably throw an error. Not that this is not really about Redux or createAsyncThunk - it would happen everywhere outside of a component.
If you need accedd to navigate outside of Redux components, see Navigating without the navigation prop
Related
So I'm very new to Prisma, and actually also to React. My Postgresql database works, but I'm trying to show the stored data in my application. My very simple table in the schema file looks like this:
model Hobby {
id Int #id #default(autoincrement())
title String
}
I'm using useContext to distribute my createHobby functionality, this is what the context file looks like.
export async function getServerSideProps() {
const hobbies: Prisma.HobbyUncheckedCreateInput[] = await prisma.hobby.findMany();
return {
props: {initialHobbies: hobbies},
};
}
export const HobbyContext = createContext({})
function Provider({ children, initialHobbies }){
const [hobbies, setHobbies] = useState<Prisma.HobbyUncheckedCreateInput[]>(initialHobbies);
const createHobby = async (title) => {
const body: Prisma.HobbyCreateInput = {
title,
};
await fetcher("/api/create-hobby", {hobby : body});
console.log(hobbies);
const updatedHobbies = [
...hobbies,
body
];
setHobbies(updatedHobbies);
const contextData = {
hobbies,
createHobby,
}
return (
<HobbyContext.Provider value={contextData}>
{children}
</HobbyContext.Provider>
);
};
export default HobbyContext;
export {Provider};
Here I get the following error Uncaught (in promise) TypeError: hobbies is not iterable at createHobby. Which refers to the const updatedHobbies = [...hobbies, body];
For more context, I have a HobbyCreate.tsx which creates a little hobby card that renders the title of the hobby, which is submitted with a form.
function HobbyCreate({updateModalState}) {
const [title, setTitle] = useState('');
const {createHobby} = useHobbiesContext();
const handleChange = (event) => {
setTitle(event.target.value)
};
const handleSubmit = (event) => {
event.preventDefault();
createHobby(title);
};
return (
...
<form onSubmit={handleSubmit}></form>
...
)
I can't really figure out what is going wrong, I assume somewhere when creating the const [hobbies, setHobbies] and using the initialHobbies.
I don't think you're using the Context API correctly. I've written working code to try and show you how to use it.
Fully typed hobby provider implementation
This is a fully typed implementation of your Provider:
import { createContext, useState } from 'react';
import type { Prisma } from '#prisma/client';
import fetcher from 'path/to/fetcher';
export type HobbyContextData = {
hobbies: Prisma.HobbyCreateInput[]
createHobby: (title: string) => void
};
// you could provide a meaningful default value here (instead of {})
const HobbyContext = createContext<HobbyContextData>({} as any);
export type HobbyProviderProps = React.PropsWithChildren<{
initialHobbies: Prisma.HobbyCreateInput[]
}>;
function HobbyProvider({ initialHobbies, children }: HobbyProviderProps) {
const [hobbies, setHobbies] = useState<Prisma.HobbyCreateInput[]>(initialHobbies);
const createHobby = async (title: string) => {
const newHobby: Prisma.HobbyCreateInput = {
title,
};
await fetcher("/api/create-hobby", { hobby: newHobby });
console.log(hobbies);
setHobbies((hobbies) => ([
...hobbies,
newHobby,
]));
};
const contextData: HobbyContextData = {
hobbies,
createHobby,
};
return (
<HobbyContext.Provider value={contextData}>
{children}
</HobbyContext.Provider>
);
}
export default HobbyContext;
export { HobbyProvider };
Using HobbyProvider
You can use HobbyProvider to provide access to HobbyContext for every component wrapped inside it.
For example, to use it in every component on /pages/hobbies your implementation would look like:
// /pages/hobbies.tsx
import { useContext, useState } from 'react';
import HobbyContext, { HobbyProvider } from 'path/to/hobbycontext';
export default function HobbiesPage() {
// wrapping the entire page in the `HobbyProvider`
return (
<HobbyProvider initialHobbies={[{ title: 'example hobby' }]}>
<ExampleComponent />
{/* page content */}
</HobbyProvider>
);
}
function ExampleComponent() {
const { hobbies, createHobby } = useContext(HobbyContext);
const [title, setTitle] = useState('');
return (
<div>
hobbies: {JSON.stringify(hobbies)}
<div>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button onClick={() => createHobby(title)}>Create hobby</button>
</div>
</div>
);
}
Similarly, to make the context available throughout your entire website, you can use HobbyProvider in
/pages/_app.tsx.
Using getServerSideProps
To retrieve the initialHobbies from the database, your getServerSideProps would look something like this:
// /pages/hobbies.tsx
import type { Hobby } from '#prisma/client';
export async function getServerSideProps() {
// note: there is no need to use `Hobby[]` as prisma will automatically give you the correct return
// type depending on your query
const initialHobbies: Hobby[] = await prisma.hobby.findMany();
return {
props: {
initialHobbies,
},
};
}
You would have to update your page component to receive the props from getServerSideProps and set initialHobbies on HobbyProvider:
// /pages/hobbies.tsx
import type { InferGetServerSidePropsType } from 'next';
export default function HobbiesPage({ initialHobbies }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<HobbyProvider initialHobbies={initialHobbies}>
<ExampleComponent />
</HobbyProvider>
);
}
Note your page component and getServerSideProps function have to be exported from the same file
I'm currently using ag-grid to render data and it works fine untill I try to edit cells using my custom cellEditorFramework component:
export default defineComponent({
name: 'LinesViewVersionEditor',
props: ['params'],
setup(props, { expose }) {
const value = ref(props.params.value)
const versionOptions = ref([])
const changedValue = ref(false)
const client = new Client({ baseURL: settings.ClientBaseUrl })
const getValue = function () {
console.log('getValue')
return value.value
}
const updateValue = function (value: { version: number; entitySlug: string; entityVersionPk: number }) {
props.params.api.stopEditing()
changedValue.value = true
}
versionOptions.value = [
{
value: value.value,
label: value.value?.version.toString()
}
]
...some code here
expose({
value,
getValue
})
return () => (
<Select
showArrow={false}
class={'ant-select-custom'}
value={value.value?.version}
options={versionOptions.value}
onChange={ value => { updateValue(value) } }
onClick={ async () => {
versionOptions.value = await getChildVersions(
client,
...args
)
}}
/>
)
}
})
As you can see I'm returning some TSX, so I'm forced to use Vue3 { expose } to return method to the parent component with agGrid table. And it has no access to exposed method & value. I tried to make different method in "methods" property of class component options and it worked as supposed. In ag-grid docs written that I can simply return getValue in setup() function but it doesn't work for me for no visible reason. Thank you in advance for help.
I use react-hook-form for the first time. I was reading the docs and followed along. Likewise, I already laid out my components and styled them. Now I am trying to alert the data out after the form submits.
This is the ContactForm
import React, { useState } from 'react';
import * as S from './style';
import { PrimaryButton } from '#element/Button';
import TextInput from '#element/TextInput';
import { useForm } from 'react-hook-form';
export const ContactForm = () => {
const { register, handleSubmit } = useForm();
const [firstName, setFirstName] = useState('');
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
return (
<S.ContactFormWrapper onSubmit={handleSubmit(onSubmit)}>
<TextInput
name={'firstName'}
label={'First Name'}
state={firstName}
setState={setFirstName}
placeholder={'John'}
type={'text'}
width={'48%'}
options={{
maxLength: '20',
minLength: '2',
required: true,
}}
register={register}
/>
<PrimaryButton type={'submit'} text={'Send Message'} />
</S.ContactFormWrapper onSubmit={handleSubmit(onSubmit)}>
)
}
This is my Custom created TextInput
import React, { useEffect, useState } from 'react';
import * as S from './style';
const TextInput = ({
name,
label,
placeholder,
state,
setState,
type,
width,
register,
options,
}) => {
const [isActive, setIsActive] = useState(false);
return (
<S.TextInputWrapper inputWidth={width}>
<S.Label htmlFor={name} isActive={isActive}>
{label}
</S.Label>
<S.Input
placeholder={placeholder}
type={type}
name={name}
id={name}
{...register(name, options)}
onChange={(event) => setState(event.target.value)}
onFocus={() => setIsActive(true)}
onBlur={() => setIsActive(false)}
/>
</S.TextInputWrapper>
);
};
export default TextInput;
Error Message
TypeError: register is not a function {...register(name, options)}
I was searching on StackOverflow there was a Post, but the Answer was confusing for me and the Questioner Code was much different than mine. Because I think the error occurred because I use styled-components, and it is nested deep. I am confused because I was reading the docs and followed along.
If I spread the Error says, register is not a function else if I not spread it then the error is ... spread is required.
Hopefully you can bring light to my confusion.
Kind regards
Kuku
The simplest solution is to take advantage of react hook form's context and use the useFormContext hook.
Input Component
import { useFormContext } from "react-hook-form";
const TextInput = ({ name, options }) => {
const { register } = useFormContext();
return (
<S.Input
name={name}
{...register(name, options)}
/>
</S.TextInputWrapper>
);
};
Remove the input register function from the parent form
export const ContactForm = () => {
...other functions
return <TextInput name={'firstName'} options={{maxLength: '20' }} />;
}
An even simpler solution is to let react-hook-form control the form values and use the useController hook or Controller component.
import { useController } from "react-hook-form";
const TextInput = ({ name, options }) => {
const { field } = useController({ name, rules: options });
return <S.Input name={name} {...field} />
};
You can also get the input states using the useContoller hook to reduce the number of events your using.
import { useController } from "react-hook-form";
const TextInput = ({ name, options }) => {
const {
field,
fieldState: { error, invalid, isDirty, isTouched }
} = useController({ name, rules: options });
};
useFormContext is a good solution explained by #Sean W
Here is another solution without useFormContext, you can use register as usual instead of passing it as a prop. You just have to forward the ref of your TextInput.
👉🏻 You can find an instance here: https://stackoverflow.com/a/68667226/4973076
I am using Protractor. The below solution works, but i get this warning:
this.currentTest.state
- error TS2532: Object is possibly 'undefined'
(property) Mocha.Context.currentTest?: Mocha.Test | undefined
How do i fix this warning?
Test file:
const helper = new HelperClass();
afterEach(async ()=> {
const state = this.currentTest.state;
await helper.getSource(state);
});
Class File
import { browser, } from 'protractor';
export class HelperClass {
public getSource(state:any) {
if (state === 'failed') {
browser.driver.getPageSource().then(function (res) {
console.log(res);
});
}
}
}
I think the error occurs because the access to this.currentTest.state happens inside another function: the arrow function passed in to afterEach--flow analysis does not cross function boundaries. Try simply pulling that line outside of the function:
const helper = new HelperClass();
afterEach(async ()=> {
const state = this.!currentTest.state;
await helper.getSource(state);
});
Does that change anything?
Given that there is not much examples about this, I am following the docs as best as I can, but the validation is not reactive.
I declare a schema :
import { Tracker } from 'meteor/tracker';
import SimpleSchema from 'simpl-schema';
export const modelSchema = new SimpleSchema({
foo: {
type: String,
custom() {
setTimeout(() => {
this.addValidationErrors([{ name: 'foo', type: 'notUnique' }]);
}, 100); // simulate async
return false;
}
}
}, {
tracker: Tracker
});
then I use this schema in my component :
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.newContext();
this.state = {
isValid: this.validation.isValid()
};
}
...
render() {
...
const errors = this.validation._validationErrors;
return (
...
)
}
}
So, whenever I try to validate foo, the asynchronous' custom function is called, and the proper addValidationErrors function is called, but the component is never re-rendered when this.validation.isValid() is supposed to be false.
What am I missing?
There are actually two errors in your code. Firstly this.addValidationErrors cannot be used asynchronously inside custom validation, as it does not refer to the correct validation context. Secondly, TrackerReact only registers reactive data sources (such as .isValid) inside the render function, so it's not sufficient to only access _validationErrors in it. Thus to get it working you need to use a named validation context, and call isValid in the render function (or some other function called by it) like this:
in the validation
custom() {
setTimeout(() => {
modelSchema.namedContext().addValidationErrors([
{ name: 'foo', type: 'notUnique' }
]);
}, 100);
}
the component
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.namedContext();
}
render() {
let errors = [];
if (!this.validation.isValid()) {
errors = this.validation._validationErrors;
}
return (
...
)
}
}
See more about asynchronous validation here.