I have a search input.
const { searchMails } = this.props;
searchMails(keyword);
I added the lodash's debounce based on this answer on Stack Overflow.
const { searchMails } = this.props;
const debounceSearchMails = debounce(searchMails, 1000);
debounceSearchMails(keyword);
The action
export const searchMails = keyword => ({ type: SEARCH_MAILS, payload: keyword });
However, after adding debounce, when I type "hello", it will still trigger 5 times searchMails after 1 second. The payload are
h
he
hel
hell
hello
How can I use debounce correctly? Thanks
UPDATE 1: add full codes
import React, { PureComponent } from 'react';
import { Field, reduxForm, reset } from 'redux-form';
import { Form } from 'reactstrap';
import debounce from 'lodash/debounce';
class Search extends PureComponent {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit(values) {
const { searchMails } = this.props;
const debounceSearchMails = debounce(searchMails, 1000);
debounceSearchMails(values.keyword);
}
render() {
const { handleSubmit, keyword } = this.props;
return (
<Form onSubmit={handleSubmit(this.onSubmit)}>
<Field name="keyword" component="input" type="search" onChange={() => setTimeout(handleSubmit(this.onSubmit))} />
</Form>
);
}
}
function validate(values) {
const errors = {};
return errors;
}
export default reduxForm({
validate,
form: 'searchForm'
})(Search);
UPDATE 2:
I changed my action to
const searchMails0 = keyword => ({ type: SEARCH_MAILS, payload: keyword });
export const searchMails = debounce(searchMails0, 1000);
But still same.
UPDATE 3: this time I changed to this, but still same.
class Search extends PureComponent {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.debouncedSubmit = debounce(this.onSubmit, 1000);
}
onSubmit(values) {
const { searchMails } = this.props;
searchMails(values.keyword);
}
render() {
const { handleSubmit, keyword } = this.props;
return (
<Form onSubmit={handleSubmit(this.debouncedSubmit)}>
<Field name="keyword" component="input" type="search" onChange={() => setTimeout(handleSubmit(this.debouncedSubmit))} />
</Form>
);
}
}
UDPATE 4:
I found the issue is somehow related with setTimeout, if I have that like below, debounce won't work. If I remove setTimeout, debounce will work. But then onChange will always return last value. So I do need have it because of redux-form's this "issue"
<Field component="input" type="search" onChange={() => setTimeout(handleSubmit(debounce(this.onSubmit, 1000)))}/>
First big thank you for #zerkms. Without his guide to the right direction, I cannot make it.
You should only create the debounced function once and then use it. It is debounced function that holds internally the state necessary for debouncing. At the moment you recreate it on every keystroke. – zerkms
After checking onChange's type, this is final working code:
class Search extends PureComponent {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onChange = this.onChange.bind(this);
this.debouncedOnChange = debounce(this.onChange, 1000);
}
onSubmit(values) {
const { searchMails } = this.props;
searchMails(values.keyword);
}
onChange(event, newValue, previousValue) {
const { searchMails } = this.props;
searchMails(newValue); // the second parameter is new value
}
render() {
const { keyword } = this.props;
return (
<Form onSubmit={handleSubmit(this.onSubmit)}>
<Field component="input" type="search" onChange={this.debouncedOnChange}/>
</Form>
);
}
}
Lessons learned:
I never thought I can do something like this.debouncedSubmit = debounce(this.onSubmit, 1000); in constructor.
And I always thought I have to use handleSubmit from redux-form, but turns out it is not for all cases.
Need go deep.
Related
When i write in the input element, it triggers onInputChange() function, that updates inputValue state, then calls the getValue() that gets the inputValue state and log in console. The value that is rendered is not the same that is in console, what's happening here?
Reproducible example: https://stackblitz.com/edit/react-i4wprk?file=src%2FApp.js
import React from 'react';
import './style.css';
export default class App extends React.Component {
constructor() {
super();
this.state = {
inputValue: '',
};
}
getValue = () => {
const { inputValue } = this.state;
console.log(inputValue);
};
onInputChange = (event) => {
const inputValue = event.currentTarget.value;
this.setState({ inputValue });
this.getValue();
};
render() {
const { inputValue } = this.state;
return (
<div>
<input placeholder="texto" onInput={this.onInputChange} />
<p>{inputValue}</p>
</div>
);
}
}
setState is not a synchronous call so there is no guarantee that your console log will fire after the value has been updated in state. You can add a callback to setState
this.setState({inputValue}, () => {this.getValue()}
I'm new to React and am tripping over this issue.
Have read couple of tutorials and questions here to find out about how Parent & Child Components should communicate. However, I am unable to get the data to populate the fields + make it editable at the same time. I'll try explain further in code below:
Parent Component:
...imports...
export default class Parent extends Component {
constructor(props) {
this.state = {
data: null
};
}
componentDidMount() {
API.getData()
.then((response) => {
this.setState({ data: response });
// returns an object: { name: 'Name goes here' }
})
}
render() {
return (
<Fragment>
<ChildComponentA data={this.state.data} />
<ChildComponentB data={this.state.data} />
</Fragment>
);
}
}
Input Hook: (source: https://rangle.io/blog/simplifying-controlled-inputs-with-hooks/)
import { useState } from "react";
export const useInput = initialValue => {
const [value, setValue] = useState(initialValue);
return {
value,
setValue,
reset: () => setValue(""),
bind: {
value,
onChange: event => {
setValue(event.target.value);
}
}
};
};
ChildComponent:* (This works to allow me to type input)
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput('');
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
ChildComponent Component:
(Trying to bind API data - Input still editable but data is still not populated even though it is correctly received.. The API data takes awhile to be received, so the initial value is undefined)
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput(props.data && props.data.name || '');
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
ChildComponent Component:
(Trying to use useEffect to bind the data works but input field cannot be typed..)
I believe this is because useEffect() is trigged every time we type.. and props.data.name is rebinding its original value
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput(props.data && props.data.name || '');
useEffect(() => {
if(props.data) {
setValue(props.data.name);
}
});
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
I can think of a few tricks like making sure it binds only once etc.. But I'm not sure if it is the correct approach. Could someone share some insights of what I could be doing wrong? And what should be the correct practice to do this.
To iterate, I'm trying to bind API data (which takes awhile to load) in parent, and passing them down as props to its children. These children have forms and I would like to populate them with these API data when it becomes available and yet remain editable after.
Thanks!
Basic way to create your Parent/Child Component structure is below, I believe. You don't need a class-based component for what you are trying to achieve. Just add an empty array as a second argument to your useEffect hook and it will work as a componentDidMount life-cycle method.
Parent component:
import React, {useState, useEffect} from 'react';
export default const Parent = () => {
const [data, setData] = useState({});
const [input, setInput] = useState({});
const inputHandler = input => setInput(input);
useEffect(() => {
axios.get('url')
.then(response => setData(response))
.catch(error => console.log(error));
}, []);
return <ChildComponent data={data} input={input} inputHandler={inputHandler} />;
};
Child Component:
import React from 'react';
export default const ChildComponent = props => {
return (
<div>
<h1>{props.data.name}</h1>
<input onChange={(e) => props.inputHandler(e.target.value)} value={props.input} />
</div>
);
};
Stack : React16, ES6, Redux
I'm currently unable to figure what's wrong here. The goal here is to add dynamically an infinite number of components (one by one) when clicking on an add button.
I need to make them appear, if possible by pair or more, e.g. if I click on the ADD button, there should be 2 fields appearing each time (one select field and one textfield at the same time, for ex)
I'm able to make the components appear, with the help of Redux, and I'm also able to manage the datas correctly (everything's wired on the back of the app)
THE PROBLEM HERE :
When trying to type text in an input field, it's ALWAYS losing the focus. I've seen that each time I update my props, the whole component named MultipleInputChoiceList is mounted again, and that each fields are re-created anew. That's what I need to fix here :
EDIT : The MultipleInputChoiceList component is mounted via a Conditional Rendering HOC (It takes some values and check if they are true, if they are, it's rendering the component without touching the whole form)
ConditionalRenderingHOC.js
import React from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
const mapStateToProps = state => {
return {
form: state.form.form
}
}
const mapDispatchToProps = dispatch => {
return {
}
}
/**
* HOC Component to check conditional rendering on form component, using requireField property
* To be enhanced at will
*/
export default (WrappedComponent, formItem = {}) => {
class ConditionalRenderingHOC extends React.Component {
componentWillMount() {
//Check if all informations are available
if (formItem.requireField !== undefined) {
const requireField = formItem.requireField
if (requireField.value !== undefined &&
requireField.name !== undefined &&
requireField.field !== undefined &&
requireField.property !== undefined) {
//If everything's here let's call canBeRendered
this.canBeRendered()
}
}
}
//Check if the count of fetched values is directly linked to the number of fetched config asked, if true, return the same number
canBeRendered() {
formItem.requireField.isRendered = false
let required = formItem.requireField
let isEqual = false
if (this.props.form[required.field] !== undefined) {
let field = this.props.form[required.field]
_.forEach(field.value, (properties, index) => {
if (properties[required.name] !== undefined) {
if (properties[required.name] === required.value) {
if (properties[required.property] === required.isEqualTo) {
formItem.requireField.isRendered = true
isEqual = true
}
}
}
})
}
return isEqual
}
render() {
let isConditionMet = this.canBeRendered()
let render = null
if (isConditionMet === true) {
render = <WrappedComponent items={formItem}/>
}
return (<React.Fragment>
{render}
</React.Fragment>)
}
}
return connect(mapStateToProps, mapDispatchToProps)(ConditionalRenderingHOC)
}
The code
//Essentials
import React, { Component } from 'react'
import _ from 'lodash'
//Material UI
import TextField from 'material-ui/TextField'
import IconButton from 'material-ui/IconButton'
import AddBox from 'material-ui/svg-icons/content/add-box'
//Components
import SelectItemChoiceList from '../form/SelectItemChoiceList'
import TextFieldGeneric from './TextFieldGeneric'
//Redux
import { connect } from 'react-redux'
import { createNewField } from '../../../actions/formActions'
const mapStateToProps = (state) => {
return {
form: state.form.form
}
}
const mapDispatchToProps = (dispatch) => {
return {
createNewField: (field, state) => dispatch(createNewField(field, state))
}
}
class MultipleInputChoiceList extends Component {
constructor(props) {
super(props)
this.state = {
inputList: [],
}
}
onAddBtnClick() {
const name = this.props.items.name
/**Create a new field in Redux store, giving it some datas to display */
this.props.createNewField(this.props.form[name], this.props.form)
}
render() {
const name = this.props.items.name
/**I think the error is around this place, as it always re-render the same thing again and again */
const inputs = this.props.form[name].inputList.map((input, index) => {
switch(input) {
case 'selectfield': {
return React.createElement(SelectItemChoiceList, {
items: this.props.form[name].multipleField[index],
key:this.props.form[name].multipleField[index].name
})
}
case 'textfield': {
return React.createElement(TextFieldGeneric, {
items: this.props.form[name].multipleField[index],
index:index,
key:this.props.form[name].multipleField[index].name
})
}
default: {
break
}
}
})
return (
<div>
<IconButton onClick={this.onAddBtnClick.bind(this)}>
<AddBox />
</IconButton>
{inputs}
</div>
)
}
}
const MultipleInputChoiceListRedux = connect(mapStateToProps, mapDispatchToProps)(MultipleInputChoiceList)
export default MultipleInputChoiceListRedux
And the TextField used here :
TextFieldGeneric.js
//Essentials
import React, { Component } from 'react';
//Components
import TextField from 'material-ui/TextField'
//Redux
import { connect } from 'react-redux'
import { validateField, isValid } from '../../../actions/formActions'
const mapStateToProps = (state) => {
return {
form: state.form.form
}
}
const mapDispatchToProps = (dispatch) => {
return {
validateField: (field) => dispatch(validateField(field)),
isValid: () => dispatch(isValid())
}
}
class TextFieldGeneric extends Component {
constructor(props) {
super(props)
this.state = {
form: {},
field: {},
index: 0
}
}
componentWillMount() {
console.log(this.props)
//first, let's load those dynamic datas before rendering
let form = this.props.form
let index = this.props.index
/** Check if there's a correctly defined parent in form (taken from the name) */
let matchName = /[a-zA-Z]+/g
let origin = this.props.items.name.match(matchName)
//form.company.value = this.getCompaniesFormChoice()
this.setState({form: form, field: form[origin], index: index})
}
//setState and check validationFields if errors
handleFieldChange(event){
const name = event.target.name
const value = event.target.value
//Change value of state form field
const item = this.props.items
item.value = value
//validate each fields
this.props.validateField(item)
//validate form
this.props.isValid()
event.preventDefault()
}
render() {
const index = this.state.index
console.log(index)
return (
<React.Fragment>
<TextField
key={index}
floatingLabelText={this.state.field.multipleField[index].namefield}
name={this.state.field.multipleField[index].namefield}
floatingLabelFixed={true}
value = {this.state.field.multipleField[index].value}
onChange = {this.handleFieldChange.bind(this)}
errorText={this.state.field.multipleField[index].error === 0 ? '' : this.state.field.multipleField[index].error}
/>
</React.Fragment>
)
}
}
const TextFieldGenericRedux = connect(mapStateToProps, mapDispatchToProps)(TextFieldGeneric)
export default TextFieldGenericRedux
I also do understand that a part of the problem lies in the render method of the parent class (MultipleInputChoiceList.js) ...
Any help or comments REALLY appreciated!
As of now, I've not come to a real answer to that question, as it's a structural problem in my data (When updating a field via a Redux action, it's re-rendering the whole component). Maybe stock those data elsewhere would be a better option.
I've only used a onBlur method on the field, that dismiss the validation I want to do on each user input, but for the moment I could not think to another viable solution.
So far, the TextFieldGeneric.js looks like that :
//setState and check validationFields if errors
handleFieldChange(event){
this.setState({value: event.target.value})
event.preventDefault()
}
handleValidation(event){
const value = this.state.value
//Change value of state form field
const item = this.props.items
item.value = value
//validate each fields
this.props.validateField(item)
//validate form
this.props.isValid()
}
render() {
return (
<React.Fragment>
<TextField
name='name'
floatingLabelFixed={true}
value = {this.state.value}
onChange = {this.handleFieldChange.bind(this)}
onBlur = {this.handleValidation.bind(this)}
/>
</React.Fragment>
)
}
If anyone have another solution, I'll gladly hear it !
Long post below, but not complicated!
I have setup my form:
NewCommentForm Component
class NewCommentForm extends Component {
render() {
const { handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit}>
<Field component="input" type="text" name="title"/>
<Field component="textarea" type="text" name="content"/>
</form>
)
}
}
const mapStateToProps = (state) => ({})
// Actions are imported as 'import * as action from '../actions/comments'
NewCommentForm = connect(mapStateToProps, actions)(NewCommentForm)
NewCommentForm = reduxForm({
form: 'newComment',
onSubmit: actions.postComment // This is the problem!
})(NewCommentForm);
RemoteSubmitButton Component
class RemoteSubmitButton extends Component {
render() {
const { dispatch } = this.props;
return (
<button
type="button"
onClick={() => dispatch(submit('newComment'))}>Submit</button>
)
}
}
RemoteSubmitButton = connect()(RemoteSubmitButton);
Everything wrapped in NewComment Component:
class NewComment extends Component {
render() {
return (
<div className="new-comment">
<NewCommentForm />
<RemoteSubmitButton />
</div>
)
}
}
The problem is with the postComment function:
export const postComment = (comment) => {
console.log("Post comment - first;") // THIS ONE GETS CALLED
return (dispatch) => {
console.log("Post comment - second"); // THIS ONE IS NEVER CALLED
return api.postComment(comment).then(response => {
dispatch({
type: 'POST_COMMENT_SUCCESS',
response
});
});
}
}
that gets its api.postComment from another file:
export const postComment = (comment) => {
return axios.post(post_comment_url, {
comment
}).then(response => {
return response;
});
}
I have redux-thunk setup in my store:
import thunk from 'redux-thunk';
const configureStore = (railsProps) => {
const middlewares = [thunk];
const store = createStore(
reducers,
railsProps,
applyMiddleware(...middlewares)
);
return store;
};
Why after submitting the form using the RemoteSubmitButton the second part of the postComment function is never called? What did I do wrong?
The problem is because you are trying to use the action that is not connected with the react-redux connect. You have to use it inside the component that is connected to the redux.
I need help. In my colorcontrol I am trying to do a this.props.dispatch(triggerFBEvent(fbID, method, params)) with no luck.
What works though is if I were to just do just triggerFBEvent(fbID, method, params). I am getting the error:
index.bundle.js:61968 Uncaught TypeError: this.props.dispatch is not a function
What I am trying to accomplish is to be able to send in new props with the line above, and then on
componentWillMount() {
this.props.dispatch(fetchFBEvent(this.props.fbID, "getColor"))
}
Call a custom service to update state with appropriate colors. But this.props.dispatch is not a function there either.
import React from 'react'
import { connect } from 'react-redux'
import {triggerFBEvent, triggerFBClearEvent, fetchFBEvent} from '../actions/functionblocksActions'
`
import { CustomPicker, HuePicker, SaturationPicker, SliderPicker, CustomPointer } from 'react-color';
#connect((store) => {
return {
fb: store.functionblocks.functionblock
}
})
export default class Functionblock extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this._getType = this._getType.bind(this)
}
_getType (wanted) {
const { fbID, fbName, func } = this.props;
let type;
let types = {
'com.xxxx.xx.service.dal.functions.Alarm': function () {
type = <Alarm fbID={fbID} fbName={fbName}/>;
},
'com.xxxx.xxx.service.dal.functions.BooleanControl': function () {
type = <BooleanControl fbID={fbID} fbName={fbName}/>;
},
'com.xxx.xxxx.service.dal.functions.BooleanSensor': function () {
type = <BooleanSensor fbID={fbID} fbName={fbName} />;
},
'com.xxxx.xxx.service.dal.functions.ColorControl': function () {
type = <ColorControl func={func} fbID={fbID} fbName={fbName} />;
}
'default': function () {
type = <WakeUp fbID={fbID} fbName={fbName} />;
}
};
// invoke it
(types[wanted] || types['default'])();
// return a String with chosen type
return type;
}
render() {
const { fbID, fbName, func } = this.props;
const type = this._getType(func.serviceProperties["clazz"]);
return(
<div>
{type}
</div>
)
}
}
// Classes for the different functions.
class ColorControl extends React.Component {
componentWillMount() {
this.props.dispatch(fetchFBEvent(this.props.fbID, "getColor"))
}
constructor(props) {
super(props);
this.state = {
color: {
h: 150.3197479248047,
s: 0.5,
l: 0.5
}
}
this.onChangeComplete = this.onChangeComplete.bind(this);
}
componentWillReceiveProps(nextProps) {
alert("YEP")
// let home = this._getHome(nextProps.areas, nextProps.statics);
// if(home!=null){
// this.setState({
// inputHome: home.name,
// })
// }
}
onChangeComplete(color, event) {
let hsl = color.hsl;
let hue = color.hsl.h / 360;
let saturation = color.hsl.s;
let lightness = color.hsl.l;
this.setState({ color: hsl })
// Update props
let fbID = this.props.fbID;
let method = "setColor";
let params = {"hue": hue, "sat": saturation, "light": lightness};
this.props.dispatch(triggerFBEvent(fbID, method, params))
}
_defineFunction(){
}
render() {
return (<div>
<SliderPicker {...this.props}
pointer={ CustomPointer }
color={this.state.color}
onChangeComplete={ this.onChangeComplete }
direction={ 'horizontal' || 'vertical' }/>
</div>
)
}
}
Can anyone help me understand whats going wrong?
You need to connect ColorControl to Redux, otherwise it doesn't get a dispatch prop.
#connect()
class ColorControl extends React.Component {
Here is the codebase to use your actions without problems.
import * as actions from './YourActionsPath'
import { bindActionCreators } from 'redux'
#connect(
state => ({
yourDerivedState : state.somePath
}),
dispatch => ({
actions : bindActionCreators( actions, dispatch )
})
)
export default class YourClass extends Component {
someMethod(){
this.props.actions.yourAction() // call it with no problems
}
render(){
return (
// your html
)
}
}
I hope you get the idea. If you use this patterns, you won't have problems.
As you can use your derived state as this.props.derivedState, which you define in the connect, you can also use your actions you defined on the connect.
Besides you can use this.props.dispatch if you connected your component. In case you need it as in your case, but this makes the code less clear and leads to maintainance problems.
import { createStore } from 'redux'
let store = createStore(//define reducer,preload state and enhancer)
//call action
store.dispatch(triggerFBEvent(fbID, method, params))