React doesn't update when parent component data retrieved from API - javascript

I'm having a functional components, where parent retrieves data from couple of arrays from API and passes them as single object to children. I get the data in parent's useEffect. Unfortunately it seems I'm not getting any data to the component, when debugging the dictionary object in child looks empty.
So in my parent tsx (cutted out nonimportant parts):
export default function RozliczeniaFaktur ({web, context, formType}: IRozliczeniaFakturProps) {
const [dataRows, setDataRows] = React.useState(emptyRowArr);
const [isError, setIsError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState(null);
const [dictionary, setDictionary] = React.useState<IDictionary>({
organizacja: [], mpk: [], vat: [], zastepstwa: [], konto: []
});
const initDictionary = () => {
let dict : IDictionary = {
organizacja: [], mpk: [], vat: [], zastepstwa: [], konto: []
};
web.lists.getByTitle("Organizacja").items.get().then((items: any[])=> {
dict.organizacja = items;
}, (e: any) => setError(`"Błąd pobierania listy Organizacja: ${e}"`));
// ommited for brevity
setDictionary(dict);
}
React.useEffect(() => {
initDictionary();
},[]);
const addRow = (rowId: number) => {
let d = dataRows;
let i = dataRows[rowId];
d.push(i);
setDataRows(d);
}
return (
<div className={styles.rozliczeniaFaktur}>
{dataRows &&
dataRows.map((dataRow, i) => {
return <TableRowControl web={web} key={i} Context={context} RowId={i} PozFaktury={dataRow} FormType={formType}
onAddRowCallback={addRow} onDeleteRowCallback={delRow} Dictionary={dictionary} />
})
}
</div>
);
}
And in child tsx:
import * as React from 'react';
import styles from './TableRowControl.module.scss';
import {IPozFaktury} from '../IPozFaktury';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { DateTimePicker, DateConvention, IDateTimePickerStrings } from '#pnp/spfx-controls-react/lib/DateTimePicker';
import { Dropdown, IDropdownOption, IDropdownStyles } from 'office-ui-fabric-react/lib/Dropdown';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { PeoplePicker, PrincipalType } from "#pnp/spfx-controls-react/lib/PeoplePicker";
import { WebPartContext } from '#microsoft/sp-webpart-base';
import { DayOfWeek } from 'office-ui-fabric-react';
import { Web } from '#pnp/sp';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import { IDictionary } from '../IDictionary';
export interface ITableRowControlProps {
RowId: Number;
PozFaktury: IPozFaktury;
FormType: String;
Context: WebPartContext;
web: Web;
Dictionary: IDictionary;
onAddRowCallback?: (...event: any[]) => void;
onDeleteRowCallback?: (...event: any[]) => void;
onErrorCallback?: (...event: any[]) => void;
}
const TableRowControl = ({RowId, Context, PozFaktury, FormType, onAddRowCallback, onDeleteRowCallback, web, Dictionary, onErrorCallback} : ITableRowControlProps) => {
const [oddzialRozliczeniaOptions, setOddzialRozliczeniaOptions] = React.useState<IDropdownOption[]>([]);
const [vatOptions, setVatOptions] = React.useState<IDropdownOption[]>([]);
const [mpkOptions, setMpkOptions] = React.useState<IDropdownOption[]>([]);
const [kontoGlOptions, setKontoGlOptions] = React.useState<IDropdownOption[]>([]);
const [zaliczkaOptions, setZaliczkaOptions] = React.useState<IDropdownOption[]>([]);
const [isPZrequired, setIsPZrequired] = React.useState(false);
return(
<tr>
<td className={styles.cell}>
<IconButton onClick={()=> onAddRowCallback(RowId)} iconProps={{ iconName: 'Add' }} title="Dodaj" ariaLabel="Dodaj" />
<IconButton onClick={()=> onDeleteRowCallback(RowId)} iconProps={{ iconName: 'Delete' }} title="Usuń" ariaLabel="Usuń" />
</td>
{/* Oddzial rozliczenia */}
<td className={styles.cell}>
<Dropdown options={Dictionary.organizacja.map(i => {return { key: i.Title, text: i.Organizacja }})} selectedKey={PozFaktury.OddzialRozliczenia} />
</td>
</tr>
);
}
export default TableRowControl;
I'm getting no options in dropdowns (works when bound to static array), and when debugging, the Dictionary object in child looks like in it's initial state in parent (with empty arrays).
EDIT:
OK, i seem to have solved the first issue, but now I've got another similar one. When I press 'Add', it looks like the code is adding a copy of a row to array dataRows (correctly), but on screen it's still one row. Where is the problem?

Related

react - not able to setValue for input box

I am making 2 otp input in my application.
In Input.tsx, I am using react-otp-input for the otp functionality
if
<OtpInput
value={"abcde"}
...
numInputs={5}
/>
The UI of react-otp-input will be
Now the problem is, when I try to change the value of otp, it throws error
Cannot set properties of undefined (setting 'value')
How can I fix it?
Input.tsx
import React, { useState } from "react";
import OtpInput from "react-otp-input";
type InputPropType = {
value: string;
setValue: (event: string) => void;
};
function Input(props: InputPropType): JSX.Element {
const { value, setValue } = props;
return (
<OtpInput
value={value}
onChange={(e: string) => {
setValue(e);
}}
numInputs={5}
/>
);
}
export default Input;
App.tsx
import React, { useState } from "react";
import Input from "./Input";
export default function App() {
type InputValueType = {
id: number;
value: string;
};
const [inputValues, setInputValues] = useState<Array<InputValueType>>([
{ id: 0, value: "" },
{ id: 1, value: "" }
]);
const InputGroup = () => {
let numOfInputs: number = 2;
var rows: Array<any> = [];
for (var i = 0; i < numOfInputs; i++) {
let inputValue: InputValueType = inputValues[i];
rows.push(
<Input
key={inputValue.id}
value={inputValue.value}
setValue={(event: string) => {
let inputValuesTemp = inputValues;
inputValuesTemp[i]["value"] = event;
setInputValues(inputValuesTemp);
}}
/>
);
}
return <>{rows}</>;
};
return (
<div className="App">
<InputGroup />
</div>
);
}
Codesandbox
https://codesandbox.io/s/react-typescript-forked-s38ck9?file=/src/App.tsx:0-918
Few things to be corrected,
There is an issue with closures in your for-loop since you have used var instead of let. All the i variables refer to the same for-loop closure in the for-loop, which means i is 2 at the end of the iteration.
All the inputValuesTemp[i] are now resolved to inputValuesTemp[2] which is definitely undefined.
Replace var with let to create closures for each iteration of the loop.
for (let i = 0; i < numOfInputs; i++) {...}
And, you also need o get a copy of the values array for react to do a rerender (to inform that the array has changed).
let inputValuesTemp = [...inputValues];
Your InputGroup component was within the App component, which caused you to lose focus for each keystroke. To fix the focus issue, move the InputGroup out of the App and keep it as a separate component.
import React, { useState } from "react";
import Input from "./Input";
type InputValueType = {
id: number;
value: string;
};
const InputGroup = () => {
const [inputValues, setInputValues] = useState<Array<InputValueType>>([
{ id: 0, value: "" },
{ id: 1, value: "" }
]);
let numOfInputs: number = 2;
var rows: Array<any> = [];
for (let i = 0; i < numOfInputs; i++) {
let inputValue: InputValueType = inputValues[i];
rows.push(
<Input
key={inputValue.id}
value={inputValue.value}
setValue={(event: string) => {
let inputValuesTemp = [...inputValues];
inputValuesTemp[i]["value"] = event;
setInputValues(inputValuesTemp);
}}
/>
);
}
return <>{rows}</>;
};
export default function App() {
return (
<div className="App">
<InputGroup />
</div>
);
}

Updating array in react state, using concat

I have a component of checkbox, basically what im trying to do is to make array of the checked ones and send the information to an api.
but im having trouble updating the state array without deleteing the last object inserted.
im using concat and still same result.
here is the code for the whole component:
import React, { Fragment, Component } from 'react';
import { Inputt, Checkbox } from './style';
interface Istate {
checked: boolean;
products?: any[];
}
interface Iprops {
id: any;
value: any;
changeValue?: (checkBoxId: string, value: any) => void;
}
class CheckBoxComp extends Component<Iprops, Istate> {
state = {
checked: true,
products: [] as any,
};
addOne = (id: any, checked: any) => {
let { products } = this.state;
const newObj = { id, checked };
products = products.concat(newObj);
this.setState({ products }, () => {
console.log(products);
});
};
isChecked = (id: any) => {
const { checked, products } = this.state;
const { changeValue } = this.props;
this.setState({
checked: !checked,
});
if (changeValue) {
changeValue(id, checked);
}
this.addOne(id, checked);
};
render() {
const { id, value } = this.props;
const { checked, products } = this.state;
return (
<Fragment>
<Checkbox>
<span />{' '}
<label className="checkbox-wrapper">
<span className="display" />
<Inputt
type="checkbox"
value={checked}
onChange={() => {
this.isChecked(id);
}}
/>
<span className="checkmark" />
</label>
</Checkbox>
</Fragment>
);
}
}
export default CheckBoxComp;
You can try this approach which utilizes the spread operator to make an array copy:
addOne = (id: any, checked: any) => {
this.setState((state: Istate): Partial<Istate> => ({
products: [
...state.products,
{ id, checked },
],
}), () => console.log(this.state.products));
};

React Jest: TypeError: Cannot read property 'map' of undefined

I am new to react and have a failing test which i am not able to understand whether it's a class import issue or I am not sending required params correctly.
Here is my code
import * as React from 'react'
import cx from 'classnames'
import './dropdown.scss'
export interface DropdownProps {
options: {
key: string
text: string
value: string
}[]
value?: string
placeholder?: string
onSelect?: (value: string) => any
children?: React.ReactNode
}
export const Dropdown = ({ options, value, placeholder, onSelect }: DropdownProps) => {
const hasValue = typeof value === 'string';
const [ top, setTop ] = React.useState(0);
const handleTriggerRef = (el: HTMLButtonElement) => {
if (el) {
setTop(el.clientHeight)
}
};
return (
<div className={cx('dropdown-container')}>
<button
className={cx('title', { hasValue: hasValue })}
ref={handleTriggerRef}
onClick={() => {
if (value && typeof onSelect === 'function') {
onSelect(value)
}
}}
>{hasValue ?
options.filter(item => item.value === value)[0].text :
(placeholder || 'Select value')}</button>
<div
style={{ top }}
className={cx('options')}
>
{options.map(option =>
<div
key={option.key}
className={cx('option')}
onClick={() => {
onSelect(option.value)
}}
><div className={cx('text')}>{option.text}</div></div>)}
</div>
</div>
)
};
And here is the test
import { shallow } from "enzyme/build";
import React from "react";
import { Dropdown } from "../dropdown";
describe('Dropdown component', () => {
test("Renders correctly", () => {
const wrapper = shallow(<Dropdown />);
expect(wrapper.exists()).toBe(true);
});
});
It is because you are not passing options to your Dropdown component.
The following will prevent the error.
{options && options.map(option => .....
But what you really will also want to do in your jest test is
<Dropdown options={options} />;
As you are using hooks it is recommended that we should use mount instead of shallow.
The error you are getting is because props are not passed to shallow render.
import { shallow } from "enzyme/build";
import React from "react";
import { Dropdown } from "../dropdown";
describe('Dropdown component', () => {
// no need to pass the optional props
const props = {
options: [{ key: 'key',text: 'text',value: 'value'}],
value: 'value',
placeholder: 'placeholder',
onSelect: jest.fn(),
children: <div>test</div>
}
test("Renders correctly", () => {
const wrapper = shallow(<Dropdown {...props}/>);
expect(wrapper.exists()).toBe(true);
});
});

How to delete a specific element from an array in the redux store

I am new to to redux and react. Still doing simple tutorials. I managed to create 2 simple components; one that outputs on the screen (as a list) whatever is in the array in the redux store, and the other component contains a button and a textfield which basically adds to that array in the store.
I would like to add a feature that will enable me to delete a specific entry in the list depending on what the user clicked on. I am thinking of creating a <button> next to each <li> tag that gets rendered as it loops through the array, and these buttons will correspond to the respective list elements. But I'm not sure how to do that.
I've tried creating a button when each <li> tag gets created but I was getting an error on the console stating that each element in a list needs a unique ID. I then decided to create another array in my store called buttons which will contain a unique id as well as the id of the list but it got out of hand. I think I might be overcomplicating this. This is what I have at the moment:
Components:
List.jsx (responsible for outputting the list)
import React from 'react'
import { connect } from "react-redux";
const ListComp = ({ lists }) => (
<div>
<ul>
{console.log(lists)}
{lists.map( element => (
<li key={element.id}>
{element.titleToBeAddedToList}
</li>
))}
</ul>
</div>
)
const mapStateToProps = state => {
return {
lists: state.lists
};
}
const List = connect(mapStateToProps)(ListComp)
export default List;
SubmitButton.jsx (responsible for outputting the button and textfield)
import React from 'react'
import { connect } from "react-redux";
import uuidv1 from "uuid";
import { addList } from "../actions/index";
import { addButton } from "../actions/index"
function mapDispatchToProps(dispatch){
return {
addlist: article => dispatch(addList(article)),
addbutton: idOfButton => dispatch(addButton(idOfButton))
};
}
class Submit extends React.Component{
constructor(){
super();
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({ [event.target.id]: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
const {titleToBeAddedToList} = this.state;
const id = uuidv1();
const button_id = uuidv1();
//Dispatching the action:
this.props.addlist({ titleToBeAddedToList, id });
this.props.addbutton({id, button_id});
//Once we've dispatched an action, we want to clear the state:
this.setState({ titleToBeAddedToList: "" });
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
className="form-control"
id="titleToBeAddedToList"
onChange={this.handleChange}
/>
</div>
<button type="submit" className="btn btn-success btn-lg">
SAVE
</button>
</form>
);
}
}
const SubmitButton = connect(null, mapDispatchToProps)(Submit)
export default SubmitButton;
Reducers:
const initialState = {
lists: [],
buttons: []
};
function rootReducer (state = initialState, action) {
if(action.type === "ADD_LIST" ){
return Object.assign({}, state, {
lists: state.lists.concat(action.payload)
});
} else if(action.type === "ADD_BUTTON"){
return Object.assign({}, state, {
buttons: state.lists.concat(action.payload)
});
} else if(action.type === "DELETE_FROM_LIST"){
//.....//
}
return state;
}
export default rootReducer;
Action:
export function addList(payload) {
return { type: "ADD_LIST", payload }
};
export function addButton(payload){
return {type: "ADD_BUTTON", payload }
}
export function deleteList(payload){
return { type: "DELETE_FROM_LIST", payload }
}
Store:
import { createStore } from "redux";
import rootReducer from "../reducers/index";
const store = createStore(rootReducer);
export default store;
You can use Math.random() as an unique key identifier, if the button is click it will call action deleteItem with the ID, action is bound to reducer pass on the ID, you can then use the ID to indentify elements and remove it in the list.
import React from 'react'
import { connect } from "react-redux";
import { deleteItem } from './actions';
const ListComp = ({ lists }) => (
<div>
<ul>
{console.log(lists)}
{lists.map( element => (
<li key={Math.random()} key={element.id}>
{element.titleToBeAddedToList}
<button onClick={() => deleteItem(element.id)}>X</button>
</li>
))}
</ul>
</div>
)
const mapStateToProps = state => {
return {
lists: state.lists
};
}
const List = connect(mapStateToProps, {deleteItem})(ListComp) // Make it available to component as props
export default List;
Action:
export function deleteElement(id) {
return function(dispatch) {
return dispatch({type: "DELETE_FROM_LIST", payload: id})
}
}
Reducer:
case 'DELETE_FROM_LIST': {
const id = action.payload;
return {
...state,
list: state.list.filter(item => item.id !== id)
}
}
else if (action.type === "DELETE_FROM_LIST") {
return Object.assign({}, state, {
buttons: state.lists.filter(item => (item.id !==action.payload))
});
}
you can use filter() for delete.
This is a minimal working react-redux example containing all the pieces to delete an item from an array in redux store.
// reducer.js
const reducer = (state, action) => {
switch (action.type) {
case 'DELETE':
return state.filter(item => (
item.id !== action.payload.id
))
default: return state;
}
}
// Item.js
const Item = ({id, onClick, label}) => (
<li>
{label}
<button onClick={ () => onClick(id) }>
delete
</button>
</li>
)
// ListContainer.js
const mapStateToProps = state => ({ items: state })
const ListContainer = ReactRedux.connect(mapStateToProps)(class extends React.Component {
handleDelete = id => {
const { dispatch } = this.props;
dispatch({ type: 'DELETE', payload: { id } })
}
render() {
const { items } = this.props;
return items.map(({id, label}) => (
<Item
label={label}
id={id}
onClick={this.handleDelete}
/>
))
}
})
// Main.js
const initialState = [
{ id: 1, label: 'item 1' },
{ id: 2, label: 'item 2' },
{ id: 3, label: 'item 3' },
{ id: 4, label: 'item 4' }
]
const store = Redux.createStore(reducer, initialState);
class App extends React.Component {
render(){
return (
<ReactRedux.Provider store={store}>
<ListContainer />
</ReactRedux.Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/6.0.1/react-redux.js"></script>
<div id="root"></div>

React-Redux Refactoring Container Logic

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!

Categories