Updating from an input element in ReactJS - javascript

I'm attempting to create a page using React, whereby I can update a single element of the state; here is how the state is defined:
interface MyState {
data?: MyData;
loading: boolean;
}
interface MyData {
id: number;
description: string;
test: string;
}
I have the following inside my render function:
return <div>
<h1>Description: {myData.description}</h1>
<br/><br/>
<input type="text" value={emailType!.test} onChange={this.handleChange} />
</div>;
And the handleChange (which is the heart of my issue):
handleChange(event: React.FormEvent<HTMLInputElement>) {
this.setState({ emailType!.test: event.currentTarget.value });
}
As I'm using tsx, the function above won't even compile. However, it does illustrate what I'm trying to do. Typically, when I've called this.setState, I've done so with a full state (that is, I know the entire new state). In this case, I only want to change the contents of a single field: is that possible?

setState allows changing only top-level key in the state like
handleChange = (event: React.FormEvent<HTMLInputElement>) => {
const emailType = { ...emailType, test: event.currentTarget.value }
this.setState({ emailType })
}
Don't forget to bind your function to the proper context.
Another option is to use function as a parameter in setState:
this.setState((oldState) => {
return {
...oldState,
emailType: {
...oldState.emailType,
test: event.currentTarget.value
}
}
})

Related

Vue.js mutating input data property

I have a component called Input.vue, which basically prints label and and input of some type. It looks like this:
<template>
<div class="form-element">
<label :for="inputId" :class="['form-element-title', { 'js-focused': focused }]">{{ label }}</label>
<input :disabled="!canEdit" :type="inputType" :id="inputId" :name="inputName" v-model="inputData" :placeholder="placeholder" #focus="focused = true" #blur="focused = false">
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Input extends Vue {
#Prop({ default: () => {} }) inputId!: string
#Prop({ default: () => {} }) inputType!: string
#Prop({ default: () => {} }) inputName!: string
#Prop({ default: () => {} }) label!: string
#Prop({ default: () => {} }) placeholder!: string
#Prop({ default: () => {} }) data!: string
#Prop({ default: () => true }) canEdit!: boolean
private inputData = this.data;
private focused = false
}
</script>
It's being called from another component like this:
<input-component inputId="formDiscountName"
inputType="text"
inputName="discountName"
:label="translations['name']"
:placeholder="translations['name']"
:data="discount.name"
/>
Now the problem is that if I enter any data in this input, discount.name doesn't get any value set, because inside input-component it's instantly assigned to inputData variable and data property doesn't get any update. Now I can't mutate data property itself because of the Vue warning.
What should I do? I tried to use computed properties but I am not sure what get and set functions should look like.
One thing I can think of is to use computed property and emit an update on set function:
get inputData() {
return this.data;
}
set inputData(value: string) {
this.$root.$emit('inputUpdate', {'inputName' : this.inputName, 'inputValue' : value});
}
But emit doesn't look very clean method. Is there anything better?
You can add a watcher on the inputData. Post that there are couple of ways that discount.name can be updated.
Either emit the inputData value from the watcher and add a handler in parent component
Or
Not sure what is your usecase here but instead of passing discount.name, you can pass the discount object and then modify the name property of the object - there won't be any warning since you are modifying a property of an object.

What is the correct to create a interface for action object with react hooks and typescript

I am working with react hooks and typescript. I used useReducer() for global state. The action of the reducer function contains two properties name and data. name means the name of event or change and data will be particular data required for that particular name.
There are four value for name till now. If name "setUserData" then data should IUserData(interface). If name is setDialog then data should DialogNames(type containing two strings). And if its something else then data is not required.
//different names of dialog.
export type DialogNames = "RegisterFormDialog" | "LoginFormDialog" | "";
//type for name property in action object
type GlobalStateActionNames =
| "startLoading"
| "stopLoading"
| "setUserData"
| "setDialog";
//interface for main global state object.
export interface IGlobalState {
loading: boolean;
userData: IUserData;
dialog: DialogNames;
}
interface IUserData {
loggedIn: boolean;
name: string;
}
//The initial global state
export const initialGlobalState: IGlobalState = {
loading: false,
userData: { loggedIn: false, name: "" },
dialog: ""
};
//The reducer function which is used in `App` component.
export const GlobalStateReducer = (
state: IGlobalState,
{ name, data }: IGlobalStateAction
): IGlobalState => {
switch (name) {
case "startLoading":
return { ...state, loading: true };
case "stopLoading":
return { ...state, loading: false };
case "setUserData":
return { ...state, userData: { ...state.userData, ...data } };
case "setDialog":
return { ...state, dialog: data };
default:
return state;
}
};
//The interface object which is passed from GlobalContext.Provider as "value"
export interface GlobalContextState {
globalState: IGlobalState;
dispatchGlobal: React.Dispatch<IGlobalStateAction<GlobalStateActionNames>>;
}
//intital state which is passed to `createContext`
export const initialGlobalContextState: GlobalContextState = {
globalState: initialGlobalState,
dispatchGlobal: function(){}
};
//The main function which set the type of data based on the generic type passed.
export interface IGlobalStateAction<
N extends GlobalStateActionNames = GlobalStateActionNames
> {
data?: N extends "setUserData"
? IUserData
: N extends "setDialog"
? DialogNames
: any;
name: N;
}
export const GlobalContext = React.createContext(initialGlobalContextState);
My <App> component looks like.
const App: React.SFC = () => {
const [globalState, dispatch] = React.useReducer(
GlobalStateReducer,
initialGlobalState
);
return (
<GlobalContext.Provider
value={{
globalState,
dispatchGlobal: dispatch
}}
>
<Child></Child>
</GlobalContext.Provider>
);
};
The above approach is fine. I have to use it like below in <Child>
dispatchGlobal({
name: "setUserData",
data: { loggedIn: false }
} as IGlobalStateAction<"setUserData">);
The problem is above approach is that it makes code a little longer. And second problem is I have to import IGlobalStateAction for not reason where ever I have to use dispatchGlobal
Is there a way that I could only tell name and data is automatically assigned to correct type or any other better way. Kindly guide to to the correct path.
Using useReducer with typescript is a bit tricky, because as you've mentioned the parameters for reducer vary depending on which action you take.
I came up with a pattern where you use classes to implement your actions. This allows you to pass typesafe parameters into the class' constructor and still use the class' superclass as the type for the reducer's parameter. Sounds probably more complicated than it is, here's an example:
interface Action<StateType> {
execute(state: StateType): StateType;
}
// Your global state
type MyState = {
loading: boolean;
message: string;
};
class SetLoadingAction implements Action<MyState> {
// this is where you define the parameter types of the action
constructor(private loading: boolean) {}
execute(currentState: MyState) {
return {
...currentState,
// this is how you use the parameters
loading: this.loading
};
}
}
Because the state update logic is now encapsulated into the class' execute method, the reducer is now only this small:
const myStateReducer = (state: MyState, action: Action<MyState>) => action.execute(state);
A component using this reducer might look like this:
const Test: FunctionComponent = () => {
const [state, dispatch] = useReducer(myStateReducer, initialState);
return (
<div>
Loading: {state.loading}
<button onClick={() => dispatch(new SetLoadingAction(true))}>Set Loading to true</button>
<button onClick={() => dispatch(new SetLoadingAction(false))}>Set Loading to false</button>
</div>
);
}
If you use this pattern your actions encapsulate the state update logic in their execute method, which (in my opinion) scales better, as you don't get a reducer with a huge switch-case. You are also completely typesafe as the input parameter's types are defined by the action's constructor and the reducer can simply take any implementation of the Action interface.

Best approach for using same component for editing and adding data. Mixing component state with redux store

I'm building web app in React with Redux. It is simple device manager. I'm using the same component for adding and updating device in database. I'm not sure, if my approach is correct. Here you can find parts of my solution:
UPDATE MODE:
In componentDidMount I'm checking, if deviceId was passed in url (edit mode). If so, I'm calling redux action to get retrieve data from database. I'm using connect function, so when response arrives, It will be mapped to component props.
Here is my mapStateToProps (probably I should map only specific property but it does not matter in this case)
const mapStateToProps = state => ({
...state
})
and componentDidMount:
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
Next, componentWillReceiveProps will be fired and I will be able to call setState in order to populate inputs in my form.
componentWillReceiveProps(nextProps) {
if (nextProps.devices.item) {
this.setState({
id: nextProps.devices.item.id,
name: nextProps.devices.item.name,
description: nextProps.devices.item.description
});
}
}
ADD MODE:
Add mode is even simpler - I'm just calling setState on each input change.
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
That's how my inputs looks:
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={this.state.description}
/>
I don't like this approach because I have to call setState() after receiving data from backend. I'm also using componentWillReceiveProps which is bad practice.
Are there any better approaches? I can use for example only redux store instead of component state (but I don't need inputs data in redux store). Maybe I can use React ref field and get rid of component state?
Additional question - should I really call setState on each input onChange?
To avoid using componentWillReceiveProps, and because you are using redux, you can do:
class YourComponent extends React.Component {
state = {
// ...
description: undefined,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.description === undefined && nextProps.description) {
return { description: nextProps.description };
}
return null;
}
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
// ...
render() {
let { description } = this.state;
description = description || ''; // use this value in your `TextField`.
// ...
return (
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={description}
/>
);
}
}
const mapStateToProps = (state) => {
let props = { ...state };
const { devices } = state;
if (devices && devices.item) {
props = {
...props,
id: devices.item.id,
name: devices.item.name,
description: devices.item.description,
};
}
return props;
};
export default connect(
mapStateToProps,
)(YourComponent);
You can then access id, name, and description thought this.props instead of this.state. It works because mapStateToProps will be evaluated every time you update the redux store. Also, you will be able to access description through this.state and leave your TextField as is. You can read more about getDerivedStateFromProps here.
As for your second question, calling setState every time the input changes is totally fine; that's what's called a controlled component, and the react team (nor me) encourage its use. See here.

Reactjs/Graphql: TypeError: Object(...) is not a function

After selecting a date and time then clicking the submit button I get the following error:
TypeError: Object(...) is not a function
The object is referring to is Action which is a query so I don't understand why it is saying it is not a function. Also, please review handleSubmit event and ensure I am calling Action correctly. THanks!!
The render Component
class Calendar extends React.Component {
constructor(props) {
super(props);
this.state = {
inputValue: ""
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit = event => {
event.preventDefault();
console.log(this.state.inputValue);
this.setState({
inputValue: new Date(document.getElementById("time").value).valueOf()
});
Action({
variables: {
timestamp: this.state.inputValue
}
});
console.log(this.state.inputValue);
};
render() {
console.log(this.props);
return (
<div className="Calendar">
<form onSubmit={this.handleSubmit.bind(this)}>
<label>Date/Time</label>
<input type="datetime-local" id="time" step="1" />
<input type="submit" value="Submit" />
</form>
</div>
//{this.render(){return (<UserList />)};
);
}
}
export default graphql(Action, {
options: props => ({
variables: {
timestamp: props.inputValue
}
})
})(Calendar);
The Action query
const Action = gql`
query($timestamp: Float!) {
action(timestamp: $timestamp) {
action
timestamp
object {
filename
}
}
}
`;
This is not the correct way of refetching the query.
Instead of
Action({
variables: {
timestamp: this.state.inputValue
}
});
try
handleSubmit = event => {
event.preventDefault();
console.log(this.state.inputValue);
this.setState({
inputValue: new Date(document.getElementById("time").value).valueOf()
}, () => {
this.props.data.refetch({
timestamp: +this.state.inputValue
});
console.log(this.state.inputValue);
});
};
if you don't want to call this props data you can rename it on your HOC graphql, like this:
export default graphql(Action, {
name: 'WHATEVERNAME'
options: props => ({
variables: {
timestamp: props.inputValue
}
})
})(Calendar);
then you would be calling this.props.WHATEVERNAME instead of this.props.data
hope it helps :D
by the way, you are binding your handleSubmit method 3 times. You need to do it only once:
binding in render is not advised because you would be processing the bind every rerender:
So you will probably want to change <form onSubmit={this.handleSubmit.bind(this)}> to <form onSubmit={this.handleSubmit}>
you can bind it on the constructor as you did. That is ok. But it is a little verbose. I would remove that.
3.handleSubmit = event => { by declaring the method as an arrow function as you did the method is automatically binded to this. So I would keep that and remove the other 2 :)
PS: note that on number 3 if you were to write handleSubmit(e) {} it would not be binded to this so you would need one of the other 2 approaches. But again, number 3 is the most efficient way to bind :) Just remove the other 2 and you are good to go.

React this.setState With Arrow Function Causes Error In Console

I have a very simple form where I'm storing a users email in the component's state and updating the state with an onChange function. There is a strange thing that is occurring where if my onChange function updates the state with a function I get two errors in the console whenever I'm typing. If I update the state with an object, however, I get no errors. I believe updating with a function is the recommended method so I'm curious to know why I'm getting these errors.
My Component:
import * as React from 'react';
import { FormGroup, Input, Label } from 'reactstrap';
interface IState {
email: string;
}
class SignUpForm extends React.Component<{}, IState> {
constructor(props: {}) {
super(props);
this.state = {
email: ''
};
}
public onEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState(() => ({ email: event.currentTarget.value }))
};
// Using this function instead of the one above causes no errors
// public onEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// this.setState({ email: event.currentTarget.value })
// };
public render() {
return (
<div>
<h1>Sign Up</h1>
<div className='row' style={{ paddingTop: '20px' }}>
<div className='col-lg-4 offset-lg-4'>
<form>
<FormGroup style={{ textAlign: 'left' }}>
<Label>Email</Label>
<Input
value={this.state.email}
onChange={this.onEmailChange}
type='text'
placeholder='Email Address'
/>
</FormGroup>
</form>
</div>
</div>
</div>
);
}
}
export default SignUpForm;
The error messages I get are:
index.js:2178 Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the method `currentTarget` on a released/nullified synthetic event. This is a no-op function. If you must keep the original synthetic event around, use event.persist(). See react-event-pooling for more information.
index.js:2178 Warning: A component is changing a controlled input of type text to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: react-controlled-components
If your state update is derived from what is currently in your state (e.g. incrementing a count variable) you should use the update function version of setState.
If you are just setting a completely new value like you do with an event handler, you don't need to use the update function version. The commented out version in your question is perfectly fine.
If you want to use the update function version you must either use event.persist() so that you can use the event asynchronously, or simply extract the value before you call setState.
public onEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.currentTarget;
this.setState(() => ({ email: value }))
};
You can't use event or any of its descendant properties once the event handler has terminated. Instead, you have to grab the value, then use the function (or if you prefer, use persist, as Tholle points out):
public onEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.currentTarget;
this.setState(() => ({ email: value }));
};
That said, since you're not updating state based on state or props, this is one of the few situations where using the non-callback version of setState is fine (details):
public onEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ email: event.currentTarget.value });
};

Categories

Resources