best way to abstract forms and inputs using React functional components? - javascript

I am kind of new to React and especially to the hooks and functional components,
I building a website, and like any other website, I need to avoid repeating myself, and abstract my code for reuse,
I created abstracted forms using classes and inherited the form and added on it like this:
class InputForm extends React.Component {
state: {}
validate = () => {
const { error } = Joi.validate(this.state.data, this.schema)
if (!error) {
return null
} else {
const errors = {}
error.details.map((oneError) => {
errors[oneError.path[0]] = oneError.message
return errors
})
return errors
}
}
validateProperty = ({ name, value }) => {
const obj = { [name]: value }
const schema = Joi.object({ [name]: this.schema[name] })
const errors = {}
const { error } = Joi.validate(obj, schema)
if (!error) {
return null
} else {
error.details.map((oneError) => {
errors[oneError.path[0]] = oneError.message
return errors
})
return errors ? errors : null
}
}
handleSubmit = (e) => {
console.log('calling handleSubmit line 1')
let { errors } = { ...this.state }
e.preventDefault()
if (!errors) {
console.log('no errors')
return
} else {
errors = this.validate() || {}
console.log('errors values are: ', errors)
if (Object.keys(errors).length === 0) {
console.log('calling do submit in handleSubmit')
this.doSubmit(e)
}
this.setState({ errors })
}
console.log('done submitting')
}
handleOnChange = ({ currentTarget: input }) => {
let { data, errors } = { ...this.state }
data[input.name] = input.value
errors = this.validateProperty(input) || {}
this.setState({ data, errors })
}
/// you can see I use this to renender any input
renderInputField = (
name,
label,
type,
message,
onChangeParams = this.handleOnChange,
...rest
) => {
const { data, errors } = { ...this.state }
return (
<InputField
{...rest}
name={name}
value={data[name]}
label={label}
onChange={(e) => {
this.handleOnChange(e)
onChangeParams(e)
}}
type={type}
message={message}
errors={errors[name]}
></InputField>
)
}
/// you can see I use this to renender any drop down input
renderInputFieldDD = (name, label, type, message, options, ...rest) => {
const { data, errors } = { ...this.state }
return (
<InputFieldDD
{...rest}
name={name}
value={data[name]}
label={label}
onChange={this.handleOnChange}
type={type}
message={message}
options={options}
errors={errors[name]}
></InputFieldDD>
)
}
renderButton = (label) => {
return (
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
>
{label}
</button>
)
}
}
export default InputForm
I wanted to do the same thing using functional components, I tried using HOC, but that is quite messed if I had to pass all the info I need in an input via props,
composing components using other ones with props is not as easy as inheritance using class-based components either!
I could achieve some composition and make code more reusable, but I don't know if there is anything specific that will make code more reusable with functional components!

So you want to create a form and extract whatever filled in it and store it inside an object using states.
You'll import useState() from react and create an empty object
import {useState} from 'react';
const FormComponent = () =>{
const [data,setData] = useState({name:"",age:""})
// this hook returns 2 values, the state and a function to update it that takes a single argument that is the updated state value
}
Create a form with onChange attribute in every input tag inside FormComponent defined earlier.
const FormComponent = () =>{
return (
<form>
<input type="text" value={data.name} placeholder="Name" onChange={(e)=>setData({...data,data.name:e.target.value})}/>
<input type="text" value={data.age} placeholder="Age" onChange={(e)=>setData({...data,data.age:e.target.value})}/>
</form>
)
}
In the above code the e.target.value extracts the value from the html tag that is the input tag here using the event object. The onChange attribute triggers every time you change something or type in the input tag.
You can also refer to this code here

Related

Using args or props to derive data from Redux using selectors

TL;DR: I'm trying to use an id and type parameters in my selectors but the params are undefined. What is the correct way of doing this using reselect and createStructuredSelector?
I'm writing selectors using reselect to get the data needed for a React component. The data is stored in a key/value format where the key is made up of a dynamic id and type value. The object looks like this:
customView: {
'viewBy:deviceProfileId&id:5923f82a-80c2-4c88-bd0e-c105ad989ab2': { // type = 'deviceProfileId' & id = '5923f82a-80c2-4c88-bd0e-c105ad989ab2'
list: { ... },
meta: { ... },
error: { ... }
}
Users enter the id and type and trigger the data fetching from API. I have the basic selector but I'm stuck trying to get the data needed for each entry using these dynamic values - they continue to come out undefined. What am I missing? Is there a better way to accomplish this?
// reducer.js : Fn to generate key
export const customViewKey = ({ customViewType, id } = {}) => {
const typeKey = (customViewType && `viewBy:${customViewType}`) || '';
const idKey = (id && `id:${id}`) || '';
const namespace = `${typeKey}&${idKey}`;
return `${namespace}`;
};
// selector.js
const getCustomView = ({ customView }) => customView; // Basic selector works
export const getCustomViewData = createSelector(
getCustomView,
(customView, id, customViewType) => { // id and customViewType are undefined
return customView && customView[customViewKey({ customViewType, id })];
}
);
// index.js
export const CustomViewsPage = ({ classes, getCustomView, customViewMap, customViewType, id }) => {
const [idValue, setIdValue] = useState('');
const [type, setType] = useState('');
const handleTypeChange = (e) => {
let typeEntered = e ? e.value : '';
setType(typeEntered);
};
const handleInputChange = (e) => {
setIdValue(e.target.value);
};
const handleSubmit = (e) => { // Fn called when user hits submit
e.preventDefault();
getCustomView({ id: idValue, customViewType: type });
};
return (
<div>
<form onSubmit={handleSubmit} className={classes.customViewForm}>
<div>
<p>Select type of custom view:</p>
<Select
placeholder="View messages by"
options={customViewTypes}
onChange={handleTypeChange}
isClearable
/>
</div>
<div>
<p>Enter ID for device, project or device profile:</p>
<InputBase
placeholder="5923f82a-80c2-4c88-bd0e-c105ad989ab2"
required
onChange={handleInputChange}
fullWidth
/>
</div>
<label htmlFor="create-custom-view-btn">
<Input id="create-custom-view-btn" type="submit" />
<Button
component="span"
variant="outlined"
color="primary"
endIcon={<SendRounded />}
onClick={handleSubmit}
>
Create view
</Button>
</label>
</form>
</div>
const mapDispatchToProps = {
getCustomView: requestCustomView,
};
const mapStateToProps = createStructuredSelector({
customViewMap: getCustomViewMap,
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(CustomViewsPage));
I figured out the solution thanks to #markerikson's answer and this blog.
These are the selectors I'm using:
const getCustomView = ({ customView }) => customView;
const getId = (_, id) => id;
const getCustomViewType = (_, id, customViewType) => customViewType;
export const getCustomViewData = createSelector(
getCustomView,
getId,
getCustomViewType,
(customView, id, customViewType) => {
return customView && customView[customViewKey({ customViewType, id })]; // WORKS!
}
);
export const getCustomViewMap = createSelector(getCustomViewData, getDomainMap);
And I'm using useSelector to call the selector from my component this way:
const selectedCustomView = useSelector(state => getCustomViewMap(state, idValue, type));
Yeah, your createSelector call is wrong.
If you want your "output function" to take 3 arguments, then you need to write 3 separate "input functions". Each input function should extract and return a value, and those become the arguments for the output function.
So, you need something like this:
export const getCustomViewData = createSelector(
getCustomView,
// these should also read values from `state` or any other args
getId,
getCustomViewType,
(customView, id, customViewType) => {
// return calculation results here
}
);
See the Redux docs Deriving Data with Selectors usage guide page for more details on how to use Reselect.

React Jsx set checked state to false (reset button)

Here I'm trying to reset selected radio buttons on this list,
however it doesn't work because
I previously change input check from {checked} to {user.checked}. Refer from UserListElement.tsx below
Therefore, I tried the following two methods.
in useEffect(), set user.userId = false
useEffect(() => {
user.checked = false;
}, [isReset, user]);
→ no change.
setChecked to true when addedUserIds includes user.userId
if (addedUserIds.includes(`${user.userId}`)) {
setChecked(true);
}
→ Unhandled Runtime Error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Any suggestion on how to make this this work?
UserListElement.tsx
export const UserListElement = ({
user,
handleOnMemberClicked,
isReset,
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isReset: boolean;
}) => {
const [checked, setChecked] = useState(user.checked);
const addedUserIds = addedUserList.map((item) => item.userId) || [];
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
const checkedState = e.target.checked;
setChecked(checkedState); //not called
user.checked = checkedState;
handleOnMemberClicked(checkedState, user.userId);
};
useEffect(() => {
setChecked(false);
}, [isReset, user]);
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
return (
<li>
<label className={style.checkboxLabel}>
<input
type="checkbox"
className={style.checkboxCircle}
checked={user.checked}
// checked={checked}
onChange={(e) => handleOnChange(e)}
/>
<span>{user.name}</span>
</label>
</li>
);
};
UserList.tsx
export const UserList = (props: {
showsUserList: boolean;handleClose: () => void;corporationId: string;currentUserId: string;samePerson: boolean;twj: string;
}) => {
const [isReset, setReset] = useState(false);
.......
const resetAll = () => {
setReset(!isReset);
setCount((addedUserList.length = 0));
setAddedUserList([]);
setUserName('');
};
......
return ( <
> < div > xxxxx <
ul className = {
`option-module-list no-list option-module-list-member ${style.personListMember}`
} > {searchedUserList.map((user, i) => (
<UserListElement user = { user }
handleOnMemberClicked = { handleOnMemberClicked }
isReset = { isReset }
key = {i} />
)) }
</ul>
/div>
<a className="is-secondary reservation-popup-filter-reset" onClick={resetAll}>
.....
}
UseAddUserList.tsx
export class UserDetail {
constructor(public userId: string | null, public name: string | null) {}
}
export let addedUserList: UserDetail[] = [];
export let setAddedUserList: Dispatch<SetStateAction<UserDetail[]>>;
export const useAddUserList = (idList: UserDetail[]) => {
[addedUserList, setAddedUserList] = useState(idList);
};
Further Clarification:
Default view
Searched option (showed filtered list)
I use user.checked because when using only checked, the checked state does not carry on from filtered list view to the full view (ex. when I erase searched word or close the popup).
The real answer to this question is that the state should NOT be held within your component. The state of checkboxes should be held in UsersList and be passed in as a prop.
export const UserListElement = ({
user,
handleOnMemberClicked,
isChecked
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isChecked: boolean;
}) => {
// no complicated logic in here, just render the checkbox according to the `isChecked` prop, and call the handler when clicked
}
in users list
return searchedUserList.map(user => (
<UserListElement
user={user}
key={user.id}
isChecked={addedUserIds.includes(user.id)} <-- THIS LINE
handleOnMemberClicked={handleOnMemberClicked}
/>
)
You can see that you almost had this figured out because you were doing this in the child:
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
Which indicates to you that the checkdd value is entirely dependent on the state held in the parent, which means there is actually no state to be had in the child.
Also, in React, NEVER mutate things (props or state) like - user.checked = true - that's a surefire way to leave you with a bug that will cost you a lot of time.
Hopefully this sheds some light
In your UserListElement.tsx you are setting state in render, which triggers renders the component again, and again set the state which again triggers re-render and the loop continues. Try to put your condition in the useEffect call, also you mutate props, so don't set user.checked = true. Instead call setter from the parent component, where it is defined.
useEffect(() => {
setChecked(false);
if (addedUserIds.includes(user.userId)) {
setChecked(true);
}
}, [user]);

React hook form returning blank object on submit

Here's my simplified problem:
https://codesandbox.io/s/busy-fire-mm91r?file=/src/FormWrapper.tsx
And the code:
export const FormItemWrapper = ({ children }) => {
return <FormItem>{children}</FormItem>;
};
export const FormWrapper = ({ children }) => {
const { handleSubmit } = useForm();
const onSubmit = (data) => {
console.log(data); // logs { }
};
return <Form onFinish={handleSubmit(onSubmit)}>{children}</Form>;
};
export default function App() {
const { control, watch } = useForm();
console.log(watch()); // logs { }
return (
<FormWrapper>
<FormItemWrapper>
<Controller
control={control}
name="tmp"
render={({ field }) => <Input {...field} />}
/>
</FormItemWrapper>
<FormItemWrapper>
<Button htmlType="submit">Save</Button>
</FormItemWrapper>
</FormWrapper>
);
}
The problem:
React-hook-form doesn't seem to see the data I type in. I can get it using antd, but can't with react-hook-form. Why? What am I missing?
watch() logs only once, and it logs { }. onSubmit logs { }
You have created two different form instances with useForm call. If you want to get current form context inside Controller you should use useFormContext and wrap your form in FormProvider.
Working example:
https://codesandbox.io/s/admiring-lehmann-mgd0i?file=/src/App.tsx

In React hook, no e.target in handleChange with setValue()

Since I'm learning how to build React forms with hooks, I went through the 3 quicks posts that culminate with this one. Everything is going well until I get to the last step when you create your custom hook with:
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
};
}
The Input is:
const Input = ({ type, name, onChange, value, ...rest }) => (
<input
name={name}
type={type}
value={value}
onChange={event => {
event.preventDefault();
onChange(name, event.target.value);
}}
{...rest}
/>
);
And the Form is:
const Form = () => {
const email = useFormInput("");
const password = useFormInput("");
return (
<form
onSubmit={e =>
e.preventDefault() || alert(email.value) || alert(password.value)
}
>
<Input
name="email"
placeholder="e-mail"
type="email"
{...email}
/>
<Input
name="password"
placeholder="password"
type="password"
{...password}
/>
<input type="submit" />
</form>
);
};
So in useFormInput() Chrome complains about
TypeError: Cannot read property ‘value’ of undefined at handleChange
which I'm pretty sure is pointing me to
function handleChange(e) {
setValue(e.target.value);
}
If I console.log(e) I get 'email', as expected (I think?), but if I try console.log(e.target) I get undefined. So obviously e.target.value doesn't exist. I can get it working by just using
setValue(document.getElementsByName(e)[0].value);
but I don't know what kind of issues this might have. Is this a good idea? Are there drawbacks to getting it to work this way?
Thanks
The issue comes from the onChange prop in the Input component
onChange={event => {
event.preventDefault();
onChange(name, event.target.value);
}}
you're calling onChange like this onChange(name, event.target.value); (two arguments, the first one is a string), while in your custom hook you define the callback like this
function handleChange(e) {
setValue(e.target.value);
}
it's expecting one argument, an event.
So either call onChange with one argument (the event) :
onChange={event => {
event.preventDefault();
onChange(event);
}}
or change the implementation of the callback.
Try this out:
const handleChange = e => {
const { inputValue } = e.target;
const newValue = +inputValue;
setValue(newLimit);
};
Had this issue with a calendar picker library react-date-picker using Register API. Looking at the documentation found out that there's another way of handling components that don't return the original event object on the onChange function using the Controller API.
More details on Controller API Docs
Example:
/*
* React Function Component Example
* This works with either useForm & useFormContext hooks.
*/
import { FC } from 'react'
import { Controller, useFormContext } from 'react-hook-form'
import DatePicker,{ DatePickerProps } from 'react-date-picker/dist/entry.nostyle'
const FormDateInput: FC<Omit<DatePickerProps, 'onChange'>> = ({
name,
...props
}) => {
const formMethods = useFormContext()
const { control } = formMethods ?? {}
return (
<Controller
render={({ field }) => <DatePicker {...props} {...field} />}
name={name ?? 'date'}
control={control}
/>
)
}
export default FormDateInput

POST http://localhost:3000/api/courses/[object%20Object]/units 404 (Not Found)

(Only my 3rd post here, so please excuse any blatant issues).
The following is my Unit component, a child of a Course component (courses has_many units).
import React from 'react';
import { connect } from 'react-redux';
import { getUnits, addUnit, updateUnit } from '../reducers/units';
import { Container, Header, Form } from 'semantic-ui-react';
class Units extends React.Component {
initialState = { name: ''}
state = { ...this.initialState }
componentDidUpdate(prevProps) {
const { dispatch, course } = this.props
if (prevProps.course.id !== course.id)
dispatch(getUnits(course.id))
}
handleSubmit = (e) => {
debugger
e.preventDefault()
debugger
const unit = this.state
const { dispatch } = this.props
if (unit.id) {
debugger
dispatch(updateUnit(unit))
} else {
debugger
dispatch(addUnit(unit))
this.setState({ ...this.initialState })
}
}
handleChange = (e) => {
const { name, value } = e.target
this.setState({ [name]: value })
}
units = () => {
return this.props.units.map( (unit, i) =>
<ul key={i}>
<li key={unit.id}> {unit.name}</li>
<button>Edit Module Name</button>
<button>Delete Module</button>
</ul>
)
}
render() {
const { name } = this.state
return (
<Container>
<Header as="h3" textAlign="center">Modules</Header>
{ this.units() }
<button>Add a Module</button>
<Form onSubmit={this.handleSubmit}>
<Form.Input
name="name"
placeholder="name"
value={name}
onChange={this.handleChange}
label="name"
required
/>
</Form>
</Container>
)
}
}
const mapStateToProps = (state) => {
return { units: state.units, course: state.course }
}
export default connect(mapStateToProps)(Units);
The following is its reducer:
import axios from 'axios';
import { setFlash } from './flash'
import { setHeaders } from './headers'
import { setCourse } from './course'
const GET_UNITS = 'GET_UNITS';
const ADD_UNIT = 'ADD_UNIT';
const UPDATE_UNIT = 'UPDATE_UNIT';
export const getUnits = (course) => {
return(dispatch) => {
axios.get(`/api/courses/${course}/units`)
.then( res => {
dispatch({ type: GET_UNITS, units: res.data, headers: res.headers })
})
}
}
export const addUnit = (course) => {
return (dispatch) => {
debugger
axios.post(`/api/courses/${course}/units`)
.then ( res => {
dispatch({ type: ADD_UNIT, unit: res.data })
const { headers } = res
dispatch(setHeaders(headers))
dispatch(setFlash('Unit added successfully!', 'green'))
})
.catch( (err) => dispatch(setFlash('Failed to add unit.', 'red')) )
}
}
export const updateUnit = (course) => {
return (dispatch, getState) => {
const courseState = getState().course
axios.put(`/api/courses/${course.id}/units`, { course })
.then( ({ data, headers }) => {
dispatch({ type: UPDATE_UNIT, course: data, headers })
dispatch(setCourse({...courseState, ...data}))
dispatch(setFlash('Unit has been updated', 'green'))
})
.catch( e => {
dispatch(setHeaders(e.headers))
dispatch(setFlash(e.errors, 'red'))
})
}
}
export default (state = [], action) => {
switch (action.type) {
case GET_UNITS:
return action.units;
case ADD_UNIT:
return [action.unit, ...state]
case UPDATE_UNIT:
return state.map( c => {
if ( c.id === action.unit.id )
return action.unit
return c
})
default:
return state;
}
};
Note: My reducer is working for my getUnits and rendering the units properly.
Note also: when I try to submit a new unit, it ignores all of the debuggers in my handleSubmit and the debuggers in my addUnits (in the reducer), but somehow renders the flash message of "Failed to add units".
Then the console logs the error seen in the title of this post.
I raked my routes and my post is definitely supposed to go to the route as it is.
I have tried passing in the unit and the course in various ways without any change to the error.
How can it hit the flash message without hitting any of the debuggers?
How do I fix this [object%20Object]issue?
Thanks in advance!
The variable course in the following line
axios.get(`/api/courses/${course}/units`)
is an object. When you try to convert an object to a string in JavaScript, [object Object] is the result. The space is then converted to %20 for the URL request.
I would look at the contents of the course variable. Likely, what you actually want in the URL is something inside of course. Perhaps course.id.
If you are still having issues, you'll need to explain what value should go in the URL between /courses/ and /units, and where that data exists.
You are invoking addUnit and updateUnit with a parameter that is equal to this.state in handleSubmit
const unit = this.state
addUnit(unit)
As this.state is of type object, it is string concatenated as object%20object.
getUnit works fine as the parameter passed there comes from the prop course. Check the value of state inside handleSubmit.

Categories

Resources