Redux-form calling submit outside form component - javascript

I'm building an application with dynamic forms (redux-form). I would like when user click on submit button to print values. Note that my submit button is placed in my application header (outside the form). To achieve this, I'm following this tutorial from Redux-form. When I press the "Save" button, I got this error in my console : (0 , _reduxForm.submit) is not a function(…).
My code :
Submit component
import React from 'react'
import {connect} from 'react-redux'
import {submit} from 'redux-form'
const RemoteSubmitButton = ({dispatch}) =>
// How to get 'form' prop here ?
<button type="submit" onClick={() => dispatch( submit() ) }>Save</button>
export default connect()(RemoteSubmitButton)
Main component
// Import librairies
import Submit from 'submitBtn'
export default class extends Component {
...
render(){
return (
// html code
<Submit form="newuser" /> // form prop gonna be dynamic
)
}
}
submit.js
import {SubmissionError} from 'redux-form'
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
function submit(values) {
return sleep(1000) // simulate server latency
.then(() => {
window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`)
})
}
export default submit
new.js (New User)
//Import librairies
import submit from 'submit'
class UserForm extends Component {
render() {
const {error, resetForm, handleSubmit} = this.props
return (<form onSubmit={ handleSubmit }>
<!-- Generate dynamic fields -->
</form>)
}
}
let FormWrapper = reduxForm({form: 'newuser', onSubmit: submit})(UserForm)
const selector = formValueSelector('newuser') // <-- same as form name
FormWrapper = connect(state => state.form)(FormWrapper)
Can you tell me what I'm doing wrong? or What can I do to make it work please ?

The submit action was added in v6.2.0 according to the release notes.
You need to upgrade your version of redux-form in order for this to work.
Edit:
In order to submit the form, you need to use the form prop in your RemoteSubmitButton component:
import React from 'react'
import {connect} from 'react-redux'
import {submit} from 'redux-form'
const RemoteSubmitButton = ({ dispatch, form }) => // Destructure the props here
<button type="submit" onClick={() => dispatch( submit(form) ) }>Save</button>
export default connect()(RemoteSubmitButton)

Related

Variable Console Logged Twice

I have recently learning react for front end development and have encountered the problem when using useStates. I tried to input a value in a textbox and submit it by clicking a submit button. Normally, only 1 response would be logged on the console, however it appeared twice.
Would be grateful if someone could spot where went wrong in my code snippet.
`
import './App.css';
import { Button } from 'react-bootstrap';
import { useState } from "react";
import axios from 'axios';
function App () {
const [key, setKey] = useState(null);
const [submit, setSubmit] = useState(false);
function getKey(val){
setKey({[val.target.name]: val.target.value})
}
{
console.log(key)
axios
.post('https://jsonplaceholder.typicode.com/posts', key)
.then(response => {
console.log(response)
})
}
return (
<>
<div className = "App">
<h1>Type in the keyword you wish to search: </h1>
<input
type = "text"
name = "keyword"
onChange = {getKey}
/>
<Button onClick = {() => setSubmit(true)} > Submit!</Button>
</div>
</>
);
}
export default App;
`

React (Next) won't re-render after redux state change (yes, state returned as new object)

I am facing a problem with re-rendering after a state change in my NextJS app.
The function sendMessageForm launches a redux action sendMessage which adds the message to the state.
The problem is unrelated to the returned state in the reducer as I am returning a new object(return {...state}) which should trigger the re-render!
Is there anything that might block the re-render ?
This is the file that calls & displays the state, so no other file should be responsible ! But if you believe the problem might lie somewhere else, please do mention !
import { AttachFile, InsertEmoticon, Mic, MoreVert } from '#mui/icons-material';
import { Avatar, CircularProgress, IconButton } from '#mui/material';
import InfiniteScroll from 'react-infinite-scroller';
import Head from 'next/head';
import { useState, useEffect } from 'react';
import Message from '../../components/Message.component';
import styles from '../../styles/Chat.module.css'
import { useRouter } from 'next/router'
import {useSelector, useDispatch} from "react-redux"
import {bindActionCreators} from "redux"
import * as chatActions from "../../state/action-creators/chatActions"
const Chat = () => {
const router = useRouter()
const { roomId } = router.query
const auth = useSelector((state)=> state.auth)
const messages = useSelector((state)=> state.chat[roomId].messages)
const dispatch = useDispatch()
const {getMessages, markAsRead, sendMessage} = bindActionCreators(chatActions, dispatch)
const [inputValue, setInputValue] = useState("")
const sendMessageForm = (e) => {
e.preventDefault()
console.log("***inputValue:", inputValue)
sendMessage(roomId, inputValue)
}
const loadMessages = (page) => {
if(roomId)
getMessages(roomId, page)
}
//user-read-message
useEffect(() => {
//user-read-message
markAsRead(roomId, auth.user._id)
}, [messages]);
return (
<div className={styles.container}>
<Head>
<title>Chat</title>
</Head>
<div className={styles.header}>
<Avatar/>
<div className={styles.headerInformation}>
<h3>Zabre el Ayr</h3>
<p>Last Seen ...</p>
</div>
<div className={styles.headerIcons}>
<IconButton>
<AttachFile/>
</IconButton>
<IconButton>
<MoreVert/>
</IconButton>
</div>
</div>
<div className={styles.chatContainer}>
<InfiniteScroll
isReverse={true}
pageStart={0}
loadMore={loadMessages}
hasMore={messages.hasNextPage || false}
loader={<div className={styles.loader} key={0}><CircularProgress /></div>}
>
{Object.keys(messages.docs).map((key, index)=>{
return<Message
key={index}
sentByMe={messages.docs[key].createdBy === auth.user._id}
message={messages.docs[key].msg}
/>})}
</InfiniteScroll>
<span className={styles.chatContainerEnd}></span>
</div>
<form className={styles.inputContainer}>
<InsertEmoticon/>
<input className={styles.chatInput} value={inputValue} onChange={(e)=>setInputValue(e.target.value)}/>
<button hidden disabled={!inputValue} type='submit' onClick={sendMessageForm}></button>
<Mic/>
</form>
</div>)
};
export default Chat;
useSelector requires a new object with a new reference from the object you are passing to it in order to trigger the re-render
What you're doing with return {...state} is just creating a new object for the parent object but not the nested one useSelector is using, which is in your case :
const messages = useSelector((state)=> state.chat[roomId].messages)
So, you should return the whole state as a new object WITH a new state.chat[roomId].messages object
In other words, the references for the root object & the one being used should be changed.

How to build async autocomplete component (React & TS) with MUI Autocomplete, react-hook-form and custom hook

I'm trying to build a generic autocomplete component with Material UI autocomplete component, react-hook-form and my custom hooks.
What it should do: on user input (after every letter) there should be a call to api, the list of options in autocomplete should be updated accordingly.
What is my issue: many rerenders of child and parent components (also when user's typing or deleting fast there's some lagging most likely connected with rerenders). I think there should be a way to avoid this behaviour using react-hook-form, which I'm trying to use since the autocompletes are form fields.
Can someone tell me is my approach wrong? I can't really change much the hook cointaining a call to api. Maybe someone had a similar issue already? I tried wrapping mui autocomplete with controller from react-hook-form but it didn't help much. Location component is being rerendered like 130 times when user types in what he wants and deletes it. Any ideas how to make this work?
What it currently looks like:
Form component (with RHF provider for context)
import React from 'react';
import { useForm, FormProvider } from 'react-hook-form';
export default function UserForm(): JSX.Element {
const methods = useForm();
const handleSkip = (): void => {
console.log('skipped');
};
const onSubmit = useCallback((rawFormData) => {
console.log(rawFormData);
}, []);
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Location />
<Actions>
<SubmitButton isPending={false}>Submit</SubmitButton>
</Actions>
</Form>
</FormProvider>
);
}
Locations:
import React, { useState } from 'react';
import { useFormContext } from 'react-hook-form';
export default function Location(): JSX.Element {
const { register } = useFormContext();
const [selectedCountry, setSelectedCountry] = useState(null);
const [nameOfCountry, setNameOfCountry] = useState('');
const { data: countries} = useAutocomplete(nameOfCountry);
return (
<div>
<CustomAutocomplete
autocompleteOptions={countries}
name='country'
onInputChange={setSelectedCountry}
placeholder='country'
registerInput={register()}
/>
</div>
);
}
CustomAutocomplete:
import React, { SetStateAction, Dispatch, Ref, useState, useEffect } from 'react';
import { TextField } from '#material-ui/core';
import Autocomplete from '#material-ui/lab/Autocomplete';
import { useFormContext, Controller } from 'react-hook-form';
interface Properties {
name: string;
placeholder: string;
autocompleteOptions: string[];
onInputChange?: Dispatch<SetStateAction<string>>;
onChange?: Dispatch<SetStateAction<string>>;
isLoading?: boolean;
registerInput: Ref<any>;
}
CustomAutocomplete.defaultProps = {
isLoading: false,
onChange: (value: Record<string, never>) => value,
onInputChange: (value: string) => value,
};
export default function CustomAutocomplete({
name,
placeholder,
autocompleteOptions,
onInputChange,
isLoading,
onChange,
registerInput,
}: Properties): JSX.Element {
const { errors } = useFormContext();
const options = autocompleteOptions || [];
return (
<div>
<Autocomplete
freeSolo
getOptionLabel={(option: Country | string) => option.name || option}
id="input-autocomplete"
onInputChange={(event, value) => {
onInputChange(value);
}}
openOnFocus={false}
// onChange={(event, value) => onChange && onChange(value) }
options={options}
renderInput={(parameters) => (
<TextField
{...parameters}
error={Boolean(errors && errors[name])}
helperText={errors && errors[name]?.message}
id={name}
inputRef={registerInput}
name={name}
placeholder={placeholder}
rowsMax="1"
/>
)}
/>
</div>
);
}
useAutocomplete hook:
Contains a call to api an returns object like:
{
//array of resuls
data: [],
isLoading: boolean
}

props.history is undefined when using useContext?

I've set up my context and I have a function that runs once the form is submitted handleSubmit. When I submit the form, I want the results to be shown on a separate page dashboard. I'm using history.push().
My form is wrapped in the withRouter HOC.
When I submit the form, I receive "props.history is undefined"
I also have another function that is using a match.params and I'm getting undefined as well. So I'm assuming it has to do with React Router.
I considered that perhaps my Context file is the one that needs to be wrapped with the withRouter HOC, but the file has two exports.
My Context Provider
import React, { useState, useEffect, createContext } from 'react'
const AnimeContext = createContext()
const API = "https://api.jikan.moe/v3"
const AnimeProvider = (props) => {
const urls = [
`${API}/top/anime/1/airing`,
`${API}/top/anime/1/tv`,
`${API}/top/anime/1/upcoming`,
]
// State for Anime search form
const [dataItems, setDataItems] = useState([])
const [animeSearched, setAnimeSearched] = useState(false)
// Fetch searched Anime
async function handleSubmit(e) {
e.preventDefault()
const animeQuery = e.target.elements.anime.value
const response = await fetch(`${API}/search/anime?q=${animeQuery}&page=1`)
const animeData = await response.json()
setDataItems(animeData.results)
setAnimeSearched(!animeSearched)
props.history.push('/dashboard')
}
return (
<AnimeContext.Provider value={{
topTv,
setTopTv,
topAiring,
setTopAiring,
topUpcoming,
setTopUpcoming,
dataItems,
setDataItems,
animeSearched,
setAnimeSearched,
fetching,
anime,
fetchTopAnime,
fetchAnimeDetails,
handleSubmit
}}>
{props.children}
</AnimeContext.Provider>
)
}
export { AnimeProvider, AnimeContext }
My SearchForm component
import React, { useContext } from 'react';
import { withRouter } from 'react-router-dom'
import styled from 'styled-components'
import AnimeCard from './AnimeCard/AnimeCard';
import { AnimeContext } from '../store/AnimeContext'
const SearchForm = () => {
const { dataItems, animeSearched, handleSubmit } = useContext(AnimeContext)
return (
<div>
<Form onSubmit={handleSubmit}>
<Input
type="text"
name="anime"
placeholder="Enter title"
/>
<FormButton type='submit'>Search</FormButton>
</ Form>
{animeSearched
?
<AnimeCard
dataItems={dataItems}
/>
: null}
</div>
)
}
export default withRouter(SearchForm)
you can always use useHitory hook everywhere!
import { useHistory } from 'react-router'
...
const Page = function(props) {
let history = useHistory();
...
history.push('/')
...
}
In react-router, you would get history from props if any component is rendered as a child or Route or from an ancestor that is renderd form Route and it passed the Router props to it. However it is not receiving Router props, i suggest try this one
You can use Redirect from react-router-dom
import { Redirect } from "react-router-dom";
const [redirect, setRedirect] = useState(false);
Now set the vlue of redirect to true where ever you want
setRedirect(true);
like in your case
async function handleSubmit(e) {
e.preventDefault()
const animeQuery = e.target.elements.anime.value
const response = await fetch(`${API}/search/anime?q=${animeQuery}&page=1`)
const animeData = await response.json()
setDataItems(animeData.results)
setAnimeSearched(!animeSearched)
setRedirect(true);
}
Now you can use the following for the Redirection in return function like so
if(redirect) {
return <Redirect to="/dashboard" />
} else {
return (
<Your-Component />
)

Why state always changes back to init value when redux rerender

State changes back to init value and useEffect run everytime when redux rerender. it looks like component remount after redux state change.
console print hello twice and input box change to empty when i input some text and click Search button.
import React, {useEffect, useState} from "react";
import Button from "react-bootstrap/es/Button";
import fetch from "cross-fetch";
import actions from "#/actions";
import api from "#/api";
import {useDispatch} from "react-redux";
const getData = (title, page) => dispatch => {
dispatch(actions.notice.loading());
return fetch(api.notice.list({title, page}), {method: 'POST'})
.then(response => response.json())
.then(resultBean => {
dispatch(actions.notice.success(resultBean.data.list))
}).catch(error => {
dispatch(actions.notice.failure(error));
});
};
const View = _ => {
const [title, setTitle] = useState('');
const dispatch = useDispatch();
useEffect(() => {
console.log('hello');
dispatch(getData(title, 1))
}, []);
return (
<>
<input onChange={e => setTitle(e.target.value)} value={title} type="text"/>
<Button onClick={e => dispatch(getData(title, 1))}>Search</Button>
</>
)
};
export default View;
You not showing the complete logic, in case you dispatch action and it triggers View parent to render, View may re-render or even unmount depending on the condition of its parent.
For View not to re-render, you need to memorize it:
// default is shallow comparison
export default React.memo(View);
For View to not to unmount, check its parent logic.
Also, note that using _ at const View = _ => still allows you to access it like _.value which I believe is not what you intended.

Categories