I am pretty new to react and hooks, and I'm struggling with useEffect(). I've watched all the vids and read all the docs and still can't quite wrap my head around the error I'm getting. ("onInput is not a function" when my New Article route loads). onInput points to a callback function in my form-hook.js. Why isn't it registering?
In my input.js component:
import React, { useReducer, useEffect } from 'react';
import { validate } from '../../util/validators';
import './Input.css';
const inputReducer = (state, action) => {
switch (action.type) {
case 'CHANGE':
return {
...state,
value: action.val,
isValid: validate(action.val, action.validators)
};
case 'TOUCH': {
return {
...state,
isTouched: true
}
}
default:
return state;
}
};
const Input = props => {
const [inputState, dispatch] = useReducer(inputReducer, {
value: props.initialValue || '',
isTouched: false,
isValid: props.initialValid || false
});
const { id, onInput } = props;
const { value, isValid } = inputState;
useEffect(() => {
console.log(id);
onInput(id, value, isValid)
}, [id, value, isValid, onInput]);
const changeHandler = event => {
dispatch({
type: 'CHANGE',
val: event.target.value,
validators: props.validators
});
};
const touchHandler = () => {
dispatch({
type: 'TOUCH'
});
};
//if statement to handle if you are updating an article and touch the category.... but it's valid
const element =
props.element === 'input' ? (
<input
id={props.id}
type={props.type}
placeholder={props.placeholder}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
) : (
<textarea
id={props.id}
rows={props.rows || 3}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
);
return (
<div
className={`form-control ${!inputState.isValid && inputState.isTouched &&
'form-control--invalid'}`}
>
<label htmlFor={props.id}>{props.label}</label>
{element}
{!inputState.isValid && inputState.isTouched && <p>{props.errorText}</p>}
</div>
);
};
export default Input;
useEffect(() => {onInput points to the onInput prop in NewArticle.js component where users can enter a new article.
import Input from '../../shared/components/FormElements/Input';
import { useForm } from '../../shared/hooks/form-hook';
const NewArticle = () => {
const [formState, inputHandler] = useForm({
title: {
value: '',
isValid: false
}
}, false );
return (
<Input
id="title"
element="input"
type="text"
label="Title"
onInput={inputHandler}
/> );
};
export default NewArticle;
...and then in my form-hook.js inputHandler is a callback. So, onInput points to a callback function through a prop. It was working, registering onInput as a function and then, all of a sudden it was throwing an error. What gives?
import { useCallback, useReducer } from 'react';
const formReducer = (state, action) => {
switch (action.type) {
case 'INPUT_CHANGE':
let formIsValid = true;
for (const inputId in state.inputs) {
if (!state.inputs[inputId]) {
continue;
}
if (inputId === action.inputId) {
formIsValid = formIsValid && action.isValid;
} else {
formIsValid = formIsValid && state.inputs[inputId].isValid;
}
}
return {
...state,
inputs: {
...state.inputs,
[action.inputId]: { value: action.value, isValid: action.isValid }
},
isValid: formIsValid
};
case 'SET_DATA':
return {
inputs: action.inputs,
isValid: action.formIsValid
};
default:
return state;
}
};
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity
});
const inputHandler = useCallback((id, value, isValid) => {
dispatch({
type: 'INPUT_CHANGE',
value: value,
isValid: isValid,
inputId: id
});
}, []);
const setFormData = useCallback((inputData, formValidity) => {
dispatch({
type: 'SET_DATA',
inputs: inputData,
formIsValid: formValidity
});
}, []);
return [formState, inputHandler, setFormData];
};
Thanks, ya'll.
I can give you some advice on how to restructure your code. This will ultimately solve your problem.
Maintain a single source of truth
The current state of your UI should be stored in a single location.
If the state is shared by multiple components, your best options are to use a reducer passed down by the Context API (redux), or pass down the container component's state as props to the Input component (your current strategy).
This means you should remove the Input component's inputReducer.
The onInput prop should update state in the container component, and then pass down a new inputValue to the Input component.
The DOM input element should call onInput directly instead of as a side effect.
Remove the useEffect call.
Separation of Concerns
Actions should be defined separately from the hook. Traditionally, actions are a function that returns an object which is passed to dispatch.
I am fairly certain that the useCallback calls here are hurting performance more than helping. For example inputHandler can be restructured like so:
const inputChange = (inputId, value, isValid) => ({
type: 'INPUT_CHANGE',
value,
isValid,
inputId
})
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity,
})
const inputHandler = (id, value, isValid) => dispatch(
inputChange(id, value, isValid)
)
}
Learn how to use debugger or breakpoints in the browser. You would quickly be able to diagnose your issue if you put a breakpoint inside your useEffect call.
Related
I'm having a React Error #31 which describes as "Objects are not valid as a React child."
The problem is I'm trying to do Object Destructuring for Form Validations as follows, so I'm trying to figure out the best way to solve this (unless refactoring everything to arrays?).
import { useReducer } from 'react';
const initialInputState = {
value: '',
isTouched: false,
};
const inputStateReducer = (state, action) => {
if (action.type === 'INPUT') {
return { value: action.value, isTouched: state.isTouched };
}
if (action.type === 'BLUR') {
return { isTouched: true, value: state.value };
}
if (action.type === 'RESET') {
return { isTouched: false, value: '' };
}
return inputStateReducer;
};
const useInput = (validateValue) => {
const [inputState, dispatch] = useReducer(inputStateReducer, initialInputState);
const valueIsValid = true;
const hasError = !valueIsValid && inputState.isTouched;
const valueChangeHandler = (event) => {
dispatch({ type: 'INPUT', value: event.target.value });
}
const inputBlurHandler = (event) => {
dispatch({ type: 'BLUR' });
}
const reset = () => {
dispatch({ type: 'RESET' });
}
return {
value: inputState.value,
isValid: valueIsValid,
hasError,
valueChangeHandler,
inputBlurHandler,
reset,
}
}
export default useInput;
import useInput from "../hooks/use-input";
import { useState } from 'react';
export default function InterestForms({ photoSet, onInterestLightboxChange, interestLightboxContent }) {
const alphabetOnly = (value) => /^[a-zA-Z() ]+$/.test(value);
const [isFormSubmitted, setFormSubmitted] = useState(false);
// Calling useInput and expect values in object destructuring.
// However, it seems this is causing React Error #31 since it expect values to be in different format
const {
value: nameValue,
isValid: nameIsValid,
hasError: nameHasError,
valueChangeHandler: nameChangeHandler,
inputBlurHandler: nameBlurHandler,
reset: resetName,
} = useInput(alphabetOnly);
...
Refactoring this to arrays might solve the problem. But I want to see if there's a better solution (or if I just missed something simple).
I'm using React Redux and want to be able to change the title and description of a post, using the onChange method. When only using React the way you would do this is that you keep an useState which you change whenever a change occurs, but I can't seem to get it to work with using redux in react. Instead of the state changing the original title, and description remains and cannot be changed.
From what I have read the basic idea is to have a listener on the input (onChange, usually) and have that fire a redux action. You then have the action tell the reducer to make the change to the store.
I have tried doing this, but could make it work correctly. What am I doing wrong and how do you solve it? I'm also wondering how do I specify that I want to change either title or description when using onChange, or do I simply send everything in post each time a change occurs?
This is what the redux state looks like when entering a post:
{
auth: {
isSignedIn: true,
user: {
id: '624481f22566374c138cf974',
username: 'obiwan',}
},
posts: {
'62448632b87b223847eaafde': {
_id: '62448632b87b223847eaafde',
title: 'hellothere',
desc: 'its been a long time since I heard that name...',
username: 'vorbrodt',
email: 'example#gmail.com',
categories: [],
createdAt: '2022-03-30T16:32:50.158Z',
updatedAt: '2022-03-30T16:32:50.158Z',
__v: 0
}
},
}
Here is where the onChange happens.
Post.js
import { getPostById, editPost } from "../actions";
const Post = ({ getPostById, editPost, username }) => {
const [updateMode, setUpdateMode] = useState(false);
let { id } = useParams();
let post = useSelector((state) => state.posts[id]);
const handleInputChange = (e) => {
try {
editPost(e.target.value);
} catch (err) {}
};
return (
<div className="post">
<div className="post-wrapper">
{updateMode ? (
<input
type="text"
value={post.title}
className="post-title-input"
autoFocus
onChange={(e) => handleInputChange(e)}
/>
) : (
<h1 className="post-title">
{post.title}
</h1>
)}
<div className="desc-area">
{updateMode ? (
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e)}
/>
) : (
<p className="post-desc">{post.desc}</p>
)}
</div>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return { username: state.auth.user.username };
};
export default connect(mapStateToProps, { getPostById, editPost })(Post);
Here is the action creator:
//edit post in redux state
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
And here is the reducer which is suppose to change the state.
postReducer.js
import _ from "lodash";
import { GET_POSTS, GET_POST, CREATE_POST, EDIT_POST } from "../actions/types";
function postReducer(state = {}, action) {
switch (action.type) {
case GET_POSTS:
return { ...state, ..._.mapKeys(action.payload, "_id") };
case GET_POST:
return { ...state, [action.payload._id]: action.payload };
case CREATE_POST:
return { ...state, [action.payload._id]: action.payload };
case EDIT_POST:
//here the change should occur, not sure how to specify if title or desc should
//change
return { ...state, [action.payload._id]: action.payload };
default:
return state;
}
}
export default postReducer;
Hey there something like this should be of help
const handleInputChange = (e, key, id) => {
try {
editPost({ [key]: e.target.value, id });
} catch (err) {}
};
Usage
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e, "title", post.id)}
/>
action
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
Reducer
case EDIT_POST:
//here we destructure the id and return the data without the id cause we //need it below
const {id, ...newData} = action.payload
const indexToUpdate = state.posts.find(post => post.id === id)
const newPostsData = [...state.posts]
//Here we update the actual object and its property that is in the state at //the specific value
newPostsData[indexToUpdate] = {...newPostData[indexToUpdate], {...newData}
return { ...state, posts: newPostsData};
I am using redux to update an array of characters as a user types or erases it, so that when the user correctly types the entire phrase I can set a success flag.
So far when typing in characters the redux type SET_INPUT fires off and updates my state but unfortunately my REMOVE_INPUT doesn't seem to fire off but it does however reach the action.
My Reducer:
import { GET_PHRASE, SET_LOADING, SET_INPUT, REMOVE_INPUT } from "../types";
const initialState = {
level: 1,
phrase: null,
scrambledPhrase: null,
words: [],
input: [],
goal: [],
success: false,
loading: false,
};
export const phraseReducer = (state = initialState, action) => {
switch (action.type) {
case GET_PHRASE:
return {
...state,
phrase: action.payload.sentence,
scrambledPhrase: action.payload.scrambledPhrase,
words: action.payload.phrase.split(" "),
goal: action.payload.phrase.split(""),
loading: false,
};
case SET_INPUT:
console.log("setting input");
return {
...state,
input: [...state.input, action.payload],
};
case REMOVE_INPUT:
console.log("removing input");
return {
...state,
input: [...state.input.slice(0, -1)],
};
case SET_LOADING:
return {
...state,
loading: true,
};
default:
return state;
}
};
My Actions:
import { GET_PHRASE, SET_LOADING, SET_INPUT, REMOVE_INPUT } from "../types";
import axios from "axios";
export const getPhrase = (level) => async (dispatch) => {
try {
setLoading();
await axios
.get(`MY ROUTE`)
.then((res) => {
// console.log(res);
const sentence = res.data.data.phrase;
const scrambledSentence = scramblePhrase(
res.data.data.phrase
);
dispatch({
type: GET_PHRASE,
payload: {
phrase: phrase.toLowerCase(),
scrambledPhrase: scrambledPhrase.toLowerCase(),
},
});
});
} catch (err) {
console.error(err);
}
};
// SET INPUT
export const setInput = (input) => async (dispatch) => {
try {
dispatch({
type: SET_INPUT,
payload: input,
});
} catch (err) {
console.error(err);
}
};
// REMOVE INPUT
export const removeInput = () => {
try {
console.log("remove reached in actions");
return {
type: REMOVE_INPUT,
};
} catch (err) {
console.error(err);
}
};
// SET LOADING
export const setLoading = () => {
console.log("Loading...");
return {
type: SET_LOADING,
};
};
My Component to input a character:
import React, { useState } from "react";
// redux imports
import { connect } from "react-redux";
import { setInput, removeInput } from "../redux/actions/phraseActions";
import PropTypes from "prop-types";
const Character = ({ character, hasSpace, setInput }) => {
const [success, setSuccess] = useState();
const handleChange = (e) => {
if (e.target.value === character) {
// console.log("Success");
setSuccess(true);
} else {
setSuccess(false);
}
};
const keyedDown = (e) => {
// check for space or a letter
if (e.keyCode === 32 || (e.keyCode > 64 && e.keyCode < 91)) {
setInput(String.fromCharCode(e.keyCode).toLowerCase());
}
// check for backspace
else if (e.keyCode === 8) {
removeInput();
}
};
return (
<div
className={`character ${
success ? "success" : hasSpace ? "space" : ""
}`}
>
<input
type="text"
name="name"
required
maxLength="1"
size="1"
onChange={handleChange}
onKeyDown={keyedDown}
className="input"
autoComplete="off"
></input>
</div>
);
};
Character.propTypes = {
setInput: PropTypes.func.isRequired,
removeInput: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
// define state
phrase: state.phrase,
});
export default connect(mapStateToProps, { setInput, removeInput })(Character);
In my console you can see which points I am reaching:
In your event handler you are not calling removeInput that was provided by connect (props.removeInput) but the imported removeInput that doesn't dispatch anything and just returns an action object, so I suggest changing the component definition to:
const Character = ({ character, hasSpace, setInput, removeInput }) => {
In your reducer you can do: input: state.input.slice(0, -1), because slice already returns a shallow copy of the array so no need to copy it with [...]
I have a form component, and the reference of input fields are linked to the useForm reducer with references. I have to set a initial form state after setting the input field references? I have done as below. But it is rendering thrice. How to solve this rendering issue?
import React, { useState } from 'react';
const useForm = () => {
const [ formState, setFormState ] = useState({});
const refs = useRef({});
const register = useCallback(( fieldArgs ) => ref => {
if(fieldArgs) {
const { name, validations, initialValue } = fieldArgs;
refs.current[name] = ref;
}
console.log('Register rendered');
}, []);
useEffect(() => {
console.log('Effect Rendered');
const refsKeys = Object.keys(refs.current);
refsKeys.forEach(refKey => {
if(!formState[refKey]) {
setFormState(prevState => {
return {
...prevState,
[refKey]: {
value: '',
touched: false,
untouched: true,
pristine: true,
dirty: false
}
}
});
}
});
}, [ refs ]);
return [ register ];
}
export { useForm };
And the app component as below
const App = () => {
const [ register ] = useFormsio();
return(
<form>
<input
type = 'email'
placeholder = 'Enter your email'
name = 'userEmail'
ref = { register({ name: 'userEmail' }) } />
<button
type = 'submit'>
Submit
</button>
</form>
)
}
How to solve this multiple rendering issue?
I think the issue in the code above is whenever refs changes you need to loop through all the fields in form and set the state.
Why don't you set the state in register method?
const register = useCallback(( fieldArgs ) => ref => {
if(fieldArgs) {
const { name, validations, initialValue } = fieldArgs;
if(!refs.current[name] ) {
refs.current[name] = ref;
setFormState(prevState => {
return {
...prevState,
[refKey]: {
value: '',
touched: false,
untouched: true,
pristine: true,
dirty: false
}
}
});
}
}
console.log('Register rendered');
}, []);
I got one container connected to one component. Its a select-suggestion component. The problem is that both my container and component are getting too much repeated logic and i want to solve this maybe creating a configuration file or receiving from props one config.
This is the code:
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { goToPageRequest as goToPageRequestCompetitions } from '../ducks/competitions/index';
import { getSearchParam as getSearchCompetitionsParam, getCompetitionsList } from '../ducks/competitions/selectors';
import { goToPageRequest as goToPageRequestIntermediaries } from '../ducks/intermediaries/index';
import { getSearchParam as getSearchIntermediariesParam, getIntermediariesList } from '../ducks/intermediaries/selectors';
import SelectBox2 from '../components/SelectBox2';
export const COMPETITIONS_CONFIGURATION = {
goToPageRequest: goToPageRequestCompetitions(),
getSearchParam: getSearchCompetitionsParam(),
suggestions: getCompetitionsList()
};
export const INTERMEDIARIES_CONFIGURATION = {
goToPageRequest: goToPageRequestIntermediaries(),
getSearchParam: getSearchIntermediariesParam(),
suggestions: getIntermediariesList()
};
const mapStateToProps = (state, ownProps) => ({
searchString: ownProps.reduxConfiguration.getSearchParam(state),
});
const mapDispatchToProps = (dispatch, ownProps) => ({
dispatchGoToPage: goToPageRequestObj =>
dispatch(ownProps.reduxConfiguration.goToPageRequest(goToPageRequestObj)),
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...ownProps,
search: searchParam => dispatchProps.dispatchGoToPage({
searchParam
}),
...stateProps
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(SelectBox2));
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Flex, Box } from 'reflexbox';
import classname from 'classnames';
import styles from './index.scss';
import Input from '../Input';
import { AppButtonRoundSquareGray } from '../AppButton';
import RemovableList from '../RemovableList';
const MIN_VALUE_TO_SEARCH = 5;
const NO_SUGGESTIONS_RESULTS = 'No results found';
class SelectBox extends Component {
/**
* Component setup
* -------------------------------------------------------------------------*/
constructor(props) {
super(props);
this.state = {
displayBox: false,
selection: null,
value: '',
items: [],
suggestions: [],
};
}
/**
* Component lifecycle
* -------------------------------------------------------------------------*/
componentWillMount() {
console.log(this.props);
document.addEventListener('mousedown', this.onClickOutside, false);
if (this.props.suggestionsType){
if (this.props.suggestionsType === 'competition'){
this.state.suggestions = this.props.competitionsSuggestions;
}
if (this.props.suggestionsType === 'intermediaries'){
this.state.suggestions = this.props.intermediariesSuggestions;
}
}
}
componentWillUnmount() {
console.log(this.props);
document.removeEventListener('mousedown', this.onClickOutside, false);
}
componentWillReceiveProps(nextProps){
console.log(this.props);
if (this.props.suggestionsType === 'competition') {
this.state.suggestions = nextProps.competitionsSuggestions;
}
if (this.props.suggestionsType === 'intermediaries') {
this.state.suggestions = nextProps.intermediariesSuggestions;
}
}
/**
* DOM event handlers
* -------------------------------------------------------------------------*/
onButtonClick = (ev) => {
ev.preventDefault();
const itemIncluded = this.state.items.find(item => item.id === this.state.selection);
if (this.state.selection && !itemIncluded) {
const item =
this.state.suggestions.find(suggestion => suggestion.id === this.state.selection);
this.setState({ items: [...this.state.items, item] });
}
};
onChangeList = (items) => {
const adaptedItems = items
.map(item => ({ label: item.name, id: item.itemName }));
this.setState({ items: adaptedItems });
};
onClickOutside = (ev) => {
if (this.wrapperRef && !this.wrapperRef.contains(ev.target)) {
this.setState({ displayBox: false });
}
};
onSuggestionSelected = (ev) => {
this.setState({
displayBox: false,
value: ev.target.textContent,
selection: ev.target.id });
};
onInputChange = (ev) => {
this.generateSuggestions(ev.target.value);
};
onInputFocus = () => {
this.generateSuggestions(this.state.value);
};
/**
* Helper functions
* -------------------------------------------------------------------------*/
setWrapperRef = (node) => {
this.wrapperRef = node;
};
executeSearch = (value) => {
if (this.props.suggestionsType === 'competition'){
this.props.searchCompetitions(value);
}
if (this.props.suggestionsType === 'intermediaries'){
this.props.searchIntermediaries(value);
}
};
generateSuggestions = (value) => {
if (value.length > MIN_VALUE_TO_SEARCH) {
this.executeSearch(value);
this.setState({ displayBox: true, value, selection: '' });
} else {
this.setState({ displayBox: false, value, selection: '' });
}
};
renderDataSuggestions = () => {
const { listId } = this.props;
const displayClass = this.state.displayBox ? 'suggestions-enabled' : 'suggestions-disabled';
return (
<ul
id={listId}
className={classname(styles['custom-box'], styles[displayClass], styles['select-search-box__select'])}
>
{ this.state.suggestions.length !== 0 ?
this.state.suggestions.map(suggestion => (<li
className={classname(styles['select-search-box__suggestion'])}
onClick={this.onSuggestionSelected}
id={suggestion.get(this.props.suggestionsOptions.id)}
key={suggestion.get(this.props.suggestionsOptions.id)}
>
<span>{suggestion.get(this.props.suggestionsOptions.label)}</span>
</li>))
:
<li className={(styles['select-search-box__no-result'])}>
<span>{NO_SUGGESTIONS_RESULTS}</span>
</li>
}
</ul>
);
};
renderRemovableList = () => {
if (this.state.items.length > 0) {
const adaptedList = this.state.items
.map(item => ({ name: item.name, itemName: item.id }));
return (<RemovableList
value={adaptedList}
className={classname(styles['list-box'])}
onChange={this.onChangeList}
uniqueIdentifier="itemName"
/>);
}
return '';
};
render() {
const input = {
onChange: this.onInputChange,
onFocus: this.onInputFocus,
value: this.state.value
};
return (
<Flex className={styles['form-selectBox']}>
<Box w={1}>
<div
ref={this.setWrapperRef}
className={styles['div-container']}
>
<Input
{...this.props}
input={input}
list={this.props.listId}
inputStyle={classname('form-input--bordered', 'form-input--rounded', styles.placeholder)}
/>
{ this.renderDataSuggestions() }
</div>
</Box>
<Box>
<AppButtonRoundSquareGray type="submit" className={styles['add-button']} onClick={this.onButtonClick}>
Add
</AppButtonRoundSquareGray>
</Box>
<Box>
{ this.renderRemovableList() }
</Box>
</Flex>
);
}
}
SelectBox.propTypes = {
items: PropTypes.instanceOf(Array),
placeholder: PropTypes.string,
listId: PropTypes.string,
className: PropTypes.string
};
SelectBox.defaultProps = {
items: [],
placeholder: 'Choose an option...',
listId: null,
className: ''
};
export default SelectBox;
As you see, in many places i am validating the type of suggestions and do something with that. Its suppose to be a reusable component, and this component could accept any kind of type of suggestions. If this grows, if will have very big validations and i don't want that. So i think that i want something similar to this:
const mapStateToProps = (state, ownProps) => ({
searchString: ownProps.reduxConfiguration.getSearchParam(state),
});
const mapDispatchToProps = (dispatch, ownProps) => ({
dispatchGoToPage: goToPageRequestObj =>
dispatch(ownProps.reduxConfiguration.goToPageRequest(goToPageRequestObj)),
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...ownProps,
search: searchParam => dispatchProps.dispatchGoToPage({
searchParam
}),
...stateProps
});
How can i make something similar to that?
Here are a few things to consider:
The purpose of using Redux is to remove state logic from your components.
What you've currently got has Redux providing some state and your component providing some state. This is an anti-pattern (bad):
// State from Redux: (line 22 - 24)
const mapStateToProps = (state, ownProps) => ({
searchString: ownProps.reduxConfiguration.getSearchParam(state),
});
// State from your component: (line 65 - 71)
this.state = {
displayBox: false,
selection: null,
value: '',
items: [],
suggestions: [],
};
If you take another look at your SelectBox component - a lot of what it is doing is selecting state:
// The component is parsing the state and choosing what to render (line 79 - 86)
if (this.props.suggestionsType){
if (this.props.suggestionsType === 'competition'){
this.state.suggestions = this.props.competitionsSuggestions;
}
if (this.props.suggestionsType === 'intermediaries'){
this.state.suggestions = this.props.intermediariesSuggestions;
}
}
Turns out, this is precisely what mapStateToProps() is for. You should move this selection logic to mapStateToProps(). Something like this:
const mapStateToProps = (state) => {
let suggestions = null;
switch (state.suggestionType) {
case 'competition':
suggestions = state.suggestions.competition;
break;
case 'intermediaries':
suggestions = state.suggestions.intermediaries;
break;
default:
break;
}
return {
suggestions
};
};
Every time the state updates (in Redux) it will pass new props to your component. Your component should only be concerned with how to render its part of the state. And this leads me to my next point: When your application state is all being managed by Redux and you don't have state logic in your components, your components can simply be functions (functional components).
const SelectBox3 = ({ suggestions }) => {
const onClick = evt => { console.log('CLICK!'); };
const list = suggestions.map((suggestion, index) => {
return (
<li key={index} onClick={onClick}>suggestion</li>
);
});
return (
<ul>
{list}
</ul>
);
};
Applying these patterns, you get components that are very easy to reason about, and that is a big deal if you want to maintain this code into the future.
Also, by the way, you don't need to use mergeProps() in your example. mapDispatchToProps can just return your search function since connect() will automatically assemble the final props object for you.:
const mapDispatchToProps = (dispatch, ownProps) => ({
// 'search' will be a key on the props object passed to the component!
search: searchParam => {
dispatch(ownProps.reduxConfiguration.goToPageRequest({ searchParam });
// (also, your 'reduxConfiguration' is probably something that belongs in
// the Redux state.)
}
});
I highly recommend giving the Redux docs a good read-through. Dan Abramov (and crew) have done a great job of laying it all out in there and explaining why the patterns are the way they are.
Here's the link: Redux.
Also, look into async actions and redux-thunk for dealing with asynchronous calls (for performing a search on a server, for example).
Finally let me say: you're on the right track. Keep working on it, and soon you will know the joy of writing elegant functional components for your web apps. Good luck!