Ok, I'm new to react and mobx, and I'm experiencing some issues to manipulate the store.
When I'm typing at the input, the value gets overwritten for each char typed.
The component:
#withStore
#observer
class ConfigModel extends Component {
configModel;
constructor(props) {
super(props);
this.configModel = this.props.store.configModelStore;
}
render() {
const fieldsObj = this.configModel.modelConfig;
const fieldHelpers = this.configModel.helperModelStore.modelConfig;
const callbackOnChange = this.configModel;
const campos = merge(fieldHelpers, fieldsObj); // _.merge()
return (
<Form key={'configModelForm'}>
<>
{Object.entries(campos).map((campo) => {
if (campo[1].advanced) {
return;
}
if (campo[1].type === 'input') {
return (
<InputRender
key={campo[1].id}
field={campo[1]}
onChange={callbackOnChange.valueOnChange}
/>
);
}
})}
</>
</Form>
);
}
}
And my store define some observables (some options were omitted for simplicity, like the type evaluated at the component above):
#observable modelConfig = [{
id: 'postType',
value: '',
disabled: false,
advanced: false,
},
{
id: 'pluralName',
value: '',
disabled: false,
advanced: true,
},
...
]
And also define some actions:
#action valueOnChange = (e, {id, value}) => {
this.modelConfig.filter((config, index) => {
if (config.id === id) {
this.modelConfig[index].value = value;
console.log(this.modelConfig[index].value);
}
});
The console.log() above prints:
I truly believe that I'm forgetting some basic concept there, so can someone spot what am I doing wrong?
*EDIT:
I have another component and another store that is working correctly:
#observable name = '';
#action setName = (e) => {
this.name = e.target.value;
console.log(this.name);
}
So my question is:
Why the action that targets a specific value like this.name works fine and the action that targets a index generated value like this.modelConfig[index].value doesn't works?
The problem was at the <InputRender> component that was also receiving the #observable decorator. Just removed and it worked.
// #observer <---- REMOVED THIS
class InputRender extends Component {
render() {
const item = this.props.field;
return (
<InputField
id={item.id}
label={
<InfoLabel
label={item.label}
action={item.action}
content={item.popupContent}
/>
}
placeholder={item.placeholder}
onChange={this.props.onChange}
value={item.value}
disabled={item.disabled}
error={item.error}
throwError={item.throwError}
/>
);
}
}
Related
I am trying to change an input inside a GrandChild class and a Bootstrap Table inside Parent class*. An user would change the input inside **GrandChild class then save it, so the changes are seen in the Bootstrap Table in Parent class; however, I am seeing this weird behavior where my props are changing before I call the .onChange (which is my save). I believe this is causing my inputs to not save or setting the state properly.
Data being passed down hierarchy: GrandParent => Parent => Child => GrandChild
It is occurring at the Child class's handleSave() function:
export class Child extends React.Component {
constructor(props){
this.state = {
data:this.props.data
}
}
handleChange = (name, value) => {
this.setState((prevState) => {
let newState = {...prevState};
newState.data.dataList[0][name] = value; // data
return newState;
});
};
handleSave(){
let dataList = this.state.data.dataList.slice();
console.log("dataList state-dataList:", dataList);
console.log("dataList before onChange 2:", this.props.data.dataList); //Same as dataList or this.state.data.dataList
this.props.onChange("dataList", dataList);
console.log("dataList onChange 3:", this.props.data.dataList); //Same as dataList or this.state.data.dataList
}
render() {
return (
<div>
<GrandChild data={this.state.data} onChange={this.handleChange} />
</div>
)
}
Child class's this.props.onChange gets sent back to the Parent class:
export class Parent extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
columns = [
{dataField: '..', text: '...' },
{dataField: '..', text: '...' },
{dataField: '..', text: '...' },
{dataField: '..', text: '...'}];
handleChange = (name, value) => {
this.props.onChange(name, value);
};
render() {
return (
<div>
<BootstrapTable
hover
condensed={true}
bootstrap4={true}
keyField={'id'}
data={this.props.data.dataList}
columns={this.columns}
/>
<Child data={this.props.data} onChange={this.handleChange} />
</div>
);
}
}
Then Parent class's this.props.onChange* gets sent to GrandParent Class:
export class GrandParent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {...this.props.location.state.data}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange = (name, value) => {
this.setState((prevState) => {
let newState = {};
let data = Object.assign({}, prevState.data);
data[name] = value;
newState.data = data;
return newState;
});
};
render() {
return (
<div>
<Form>
<Parent data={this.state.data} onChange={this.handleChange} />
</Form>
</div>
)
}
This is the GrandChild's class:
export class GrandChild extends React.Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ?
target.checked :
target.value;
const name = target.name;
this.props.onChange(name, value);
};
render() {
return (
<div>
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Label Name</Form.Label>
<Form.Control name="labelName" value={this.props.data.[0]labelName || ""} //ignore the index for now
onChange={this.handleInputChange}/>
</Form.Group>
</Form.Row>
</div>
)
}
}
I expected that console.logs() of the dataLists to be different; however, they give the same exact object before it even runs the this.props.onChange("dataList", dataList);
Potentially, the third dataList console.log might be same as the state dataList because of setState being asynchronous.
It looks like the main issue is that you're mutating state/props in Child:
handleChange = (name, value) => {
this.setState((prevState) => {
// {...prevState} makes a shallow copy of `prevState`
let newState = {...prevState};
// Next you're modifying data deep in `newState`,
// so it's mutating data in the `dataList` array,
// which updates the source of truth for both props and state.
newState.data.dataList[0][name] = value;
return newState;
});
};
One way to do this (avoiding mutation) is like this:
handleChange = (name, value) => {
this.setState(prevState => ({
data: {
...prevState.data,
dataList: [
{
...prevState.data.dataList[0],
[name]: value
},
...prevState.data.dataList.slice(1)
]
}
}));
};
If that's more verbose than you'd like, you could use a library like immutable-js.
Another issue that could cause you bugs is copying props into state. This article gives some explanation of why that's bad: https://overreacted.io/writing-resilient-components/#dont-stop-the-data-flow-in-rendering
Basically: If you set props in state and then update state and pass props down to a child, the data you're passing down will be stale. It doesn't look like you're doing that here, but it would be easy to miss. An easy way to avoid this is to name any props you plan on setting in state initialProp If your prop is named initialData, it will be clear that from that point in the tree you should rely on the value in state rather than props.
Also, handleChange in Grandparent can be written more simply:
handleChange = (name, value) => {
this.setState(prevState => ({
data: {
...prevState.data,
[name]: value
}
}))
};
I have a question for you... On my react app, I have a <input></input> but I would like the user to be able to keep his message thanks to the local storage.
class Profil extends Component {
message() {
if (localStorage != 'undefined'){
document.getElementById('message').value = localStorage.getItem('message');
}
else {
alert("localStorage is not supported");
}
}
render() {
return (
<div>
<input name="message" onChange={() => this.message()}></input>
</div>
);
}}
With that, when I put a letter, I have directly an error message :
TypeError: localStorage.getItem(...) is null
and this line is false :
document.getElementById('message').value = localStorage.getItem('message');
What do I have to do ?
Instead of manipulating the DOM manually, you could keep the message in state and update the localStorage value on every change, and also read the value from localStorage when the component is created.
Example
class Profile extends React.Component {
constructor() {
super();
this.state = {
message: localStorage.getItem("message") || ""
};
}
onChange = event => {
const message = event.target.value;
localStorage.setItem("message", message);
this.setState({ message });
};
render() {
return <input value={this.state.message} onChange={this.onChange} />;
}
}
In functional component in the following way we can able to store and restore the values from localstorage using useEffect hook
const mounted = useRef()
const [ formValues, setFormValues ] = useState({firstname: '', lastname: ''})
function supportsLocalStorage() {
try {
return 'sessionStorage' in window && window['sessionStorage'] !== null;
} catch(e){
return false;
}
}
useEffect(() => {
if (!mounted.current && supportsLocalStorage) {
const progress = JSON.parse(localStorage.getItem('formValues'))
if (progress !== null) {
for (const x in formValues) {
formValues[x] = progress[x]
}
}
mounted.current = true
} else {
const filteredState = formValues
localStorage.setItem('formValues', JSON.stringify(filteredState))
}
}, [ formValues ])
I have an input tag component from react-tagsinput as follows:
const onTagChange = (tags) => {
const noDuplicateTags = tags.filter((v, i) => tags.indexOf(v) === i);
const duplicateEntered = tags.length !== noDuplicateTags.length;
if (duplicateEntered) {
onTagChange(tags);
console.log('duplicate');
}
onTagChange(noDuplicateTags);
};
function TagContainer({
tags,
}) {
return (
<div>
<Header>Meta:</Header>
<TagsInput value={tags} onChange={onTagChange} />
</div>
);
}
TagContainer.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
};
TagContainer.defaultProps = {
tags: [],
};
export default TagContainer;
and the implementation on the onTagChange method which is passed as a prop to the <TagContainer> component in another component.
export class Modal extends React.Component {
...
...
onTagChange = (tags) => {
this.props.onTagChange(tags);
}
...
...
render() {
return(
<TagContainer
tags={tags}
onTagChange={this.onTagChange}
/>
);
}
}
Problem: onlyUnique prop in the <TagsInput> component is set to true to avoid duplicate entries. But I need to display an error message saying "duplicate values" as soon as user enters a duplicate value. How can this be done especially on the third party component.
I think you're going to have to handle dealing with duplicates in your component because you are getting no feedback from <TagInput /> component.
At a higher level, I would do something like this
class Example extends React.Component {
constructor() {
super();
this.state = {
showDuplicateError: false
};
}
handleTagChange(tags) {
const uniqueTags = removeDuplicates(tags);
const duplicateEntered = tags.length !== uniqueTags.length;
if (duplicateEntered) {
this.showDuplicateError();
}
// add unique tags regardless, as multiple tags could've been entered
const { onTagChange } = this.props;
onTagChange(uniqueTags);
}
showDuplicateError() {
this.setState({
showDuplicateError: true
});
}
render() {
const { showDuplicateError } = this.state;
const { tags } = this.props;
return (
<React.Fragment>
{ showDuplicateError && <div>Duplicate entered</div>}
<TagsInput value={ tags } onTagChange={ this.handleTagChange } />
</React.Fragment>
);
}
}
I am trying to make an edit page. I am update the state for any changes made. I want to compare the initial state with the last state on the last save. but I can not control the first state.
export default class extends Component {
constructor(props){
super(props);
this.changeDetails = this.changeDetails.bind(this);
this.state = {
driver: this.props.driver
}
}
changeDetails = value => {
this.setState({
driver:value
})
}
onRegister = () => {
//I want to make a comparison here.
}
render() {
const {driver} = this.state
return (
<div>
<EditView driver={driver} changeDetails={this.changeDetails}/>
</div>
);
}
}
EditView.js
export default class extends Component {
render() {
const { driver} = this.props;
const changeDetails = event => {
driver['fname] = event.target.value;
this.props.changeDetails(driver);
};
return (
<div>
<Input
value={driver.fname}
onChange={event => changeDetails(event)}
/>
</div>
);
}
}
Do not mutate driver itself directly. Use something like this:
const changeDetails = event =>
this.props.changeDetails( { ...driver, fname: event.target.value } );
I have a form with controlled inputs managed via a Higher Order Component. The structure is like this:
Field Higher Order Component
function BaseField(WrappedComponent) {
class WrappedField extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
active: false,
}
}
setValue = (e) => {
this.setState({ value: e.target.value })
}
...
<WrappedComponent
{...this.props}
value={this.state.value}
set={this.setValue}
active={this.state.active}
/>
....
Field
import React from 'react';
import BaseField from './BaseField';
const TextField = (props) => {
return <input
value={props.value}
onChange={props.set}
name={props.name}
type={props.type}
/>
}
export default BaseField(TextField);
When using TextField this works well - however, I want to use this with more flexibility - for example, I'd like to be able to enhance the onChange functionality in some cases, always having it set state but also have it do other things based on state or functions in the component the TextField is used in.
Am I misunderstanding how HOCs work?
You can use something like createChainedFunction from react-bootstrap:
function createChainedFunction(...funcs) {
return funcs
.filter(f => f != null)
.reduce((acc, f) => {
if (typeof f !== 'function') {
throw new Error('Invalid Argument Type, must only provide functions, undefined, or null.');
}
if (acc === null) {
return f;
}
return function chainedFunction(...args) {
acc.apply(this, args);
f.apply(this, args);
};
}, null);
}
and something from my react utils:
export function copyPropsWithout(props, without) {
const propKeys = Object.keys(props);
const passProps = propKeys.reduce((obj, propKey) => {
if (without.indexOf(propKey) === -1) {
obj[propKey] = props[propKey];
}
return obj;
}, {});
return passProps;
}
I'd add these to your utils and then use them like:
...
<WrappedComponent
{...copyPropsWithout(this.props, ['onChange'])}
value={this.state.value}
set={createChainedFunction(this.setValue, this.props.onChange}}
active={this.state.active}
/>
....