I'm curently working on a React app and trying to detect the time taken to reach a certain input value length.
It is a controlled input which value is stored and set via redux action and reducer.
I want to start counting time when the input value is !== "" and stop counting when the value .length is equal to 13.
Further, in the app logic, if the time taken to reach .length === 13 is something like under 100ms ( + or - ) it will mean that the app user used a barcode scanner, else, he typed the barcode with the keyboard.
i've tried to use vars with new Date() to get the time diff but the render() logic blocks the maintain of the elapsed time count...
Any idea of how i could achieve my goal ?
I leave you the component code just below,
Thank you in advance !
import React from "react";
import StoreInput from "../StoreInput/index";
import { connect } from "react-redux";
import "./index.scss";
import { setStoreInputFieldValue } from "../../actions/store.actions";
import { addArticleToStore } from "../../actions/articles.actions";
type ScanSetProps = {
// Redux State
storeInputFieldValue?: any;
// Redux Actions
setStoreInputFieldValue?: any;
addArticleToStore?: any;
};
class ScanSet extends React.Component<ScanSetProps> {
handleScanSet = (event) => {
const { setStoreInputFieldValue } = this.props;
setStoreInputFieldValue(event.target.value);
};
// Component render
render() {
const { storeInputFieldValue, addArticleToStore } = this.props;
return (
<div className="ScanSet">
<StoreInput
idStoreInput={"scanSetInput"}
typeStoreInput={"number"}
placeholderStoreInput={
"Scannez le code barre ou saisissez le code EAN"
}
storeInputFillMethod={this.handleScanSet}
/>
<button
id="scanSetButton"
className={
storeInputFieldValue.length === 13
? "enabledButton"
: "disabledButton"
}
onClick={() => addArticleToStore(storeInputFieldValue)}
>
Ajouter
</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
storeInputFieldValue: state.store.storeInputFieldValue,
});
const mapDispatchToProps = (dispatch) => ({
setStoreInputFieldValue: (input_value) =>
dispatch(setStoreInputFieldValue(input_value)),
addArticleToStore: (article_ean) => dispatch(addArticleToStore(article_ean)),
});
export default connect(mapStateToProps, mapDispatchToProps)(ScanSet);
I would recommend using state.
When input !== '',
this.setState((state) => {...state, startTime: new Date().getTime()})
When value.length === 13,
this.setState((state) => {...state, endTime: new Date().getTime()}
Then you can have another part which accounts for the difference, (endTime - startTime)
Since you're using redux, if you have a slice that accounts for this. You can simply dispatch the two actions (setStartTime, setEndTime) and let the reducer handle the logic above.
Urmzd's answer was a good approach, it solved my problem and also used the Redux Saga library each time a "SET_END_TIME" action is triggered to getTimeDiff and launch the further logic. Here is what code now looks like :
Component Code : index.tsx
import StoreInput from "../StoreInput/index";
import { connect } from "react-redux";
import "./index.scss";
import {
setStoreInputFieldValue,
setStartTime,
setEndTime,
resetTimeDiff,
} from "../../actions/store.actions";
import { handleInputEan } from "../../actions/articles.actions";
type ScanSetProps = {
// Redux State
storeInputFieldValue?: any;
// Redux Actions
setStoreInputFieldValue?: any;
handleInputEan?: any;
setStartTime?: any;
setEndTime?: any;
resetTimeDiff?: any;
};
class ScanSet extends React.Component<ScanSetProps> {
handleScanSet = (event) => {
const {
setStoreInputFieldValue,
storeInputFieldValue,
setStartTime,
setEndTime,
resetTimeDiff,
} = this.props;
setStoreInputFieldValue(event.target.value);
if (storeInputFieldValue.length + 1 === 1) {
setStartTime();
} else if (storeInputFieldValue.length + 1 === 13) {
setEndTime();
} else if (storeInputFieldValue.length - 13 === 0) {
resetTimeDiff();
}
};
// Component render
render() {
const { storeInputFieldValue, handleInputEan } = this.props;
return (
<div className="ScanSet">
<StoreInput
idStoreInput={"scanSetInput"}
typeStoreInput={"number"}
placeholderStoreInput={
"Scannez le code barre ou saisissez le code EAN"
}
storeInputFillMethod={this.handleScanSet}
/>
<button
id="scanSetButton"
className={
storeInputFieldValue.length === 13
? "enabledButton"
: "disabledButton"
}
onClick={() => handleInputEan()}
>
Ajouter
</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
storeInputFieldValue: state.store.storeInputFieldValue,
timeDiff: state.store.timeDiff,
});
const mapDispatchToProps = (dispatch) => ({
setStartTime: () => dispatch(setStartTime()),
setEndTime: () => dispatch(setEndTime()),
resetTimeDiff: () => dispatch(resetTimeDiff()),
setStoreInputFieldValue: (input_value) =>
dispatch(setStoreInputFieldValue(input_value)),
handleInputEan: () => dispatch(handleInputEan()),
});
export default connect(mapStateToProps, mapDispatchToProps)(ScanSet);
Actions Code : store.actions.js ( with consts from store.const.js )
import * as storeConst from "../const/store.const";
export const setStartTime = () => ({
type: storeConst.SET_START_TIME,
payload: new Date().getTime(),
});
export const setEndTime = () => ({
type: storeConst.SET_END_TIME,
payload: new Date().getTime(),
});
export const getTimeDiff = () => ({
type: storeConst.GET_TIME_DIFF,
});
export const resetTimeDiff = () => ({
type: storeConst.RESET_TIME_DIFF,
});
Reducers Code : store.reducer.js ( with consts from store.const.js and reducers combined in an index.js file)
import * as storeConst from "../const/store.const";
const initState = {
startTime: null,
endTime: null,
timeDiff: null,
};
const store = (state = initState, action) => {
switch (action.type) {
case storeConst.SET_START_TIME:
return { ...state, startTime: action.payload };
case storeConst.SET_END_TIME:
return { ...state, endTime: action.payload };
case storeConst.GET_TIME_DIFF:
return { ...state, timeDiff: state.endTime - state.startTime };
case storeConst.RESET_TIME_DIFF:
return { ...state, timeDiff: null };
default:
return state;
}
};
export default store;
Redux Saga Code : store.saga.js ( combined: in an index.js file as rootSaga)
import { put } from "redux-saga/effects";
import { store } from "../store";
import {
getTimeDiff,
resetTimeDiff,
} from "../actions/store.actions";
import {
handleInputEan,
} from "../actions/articles.actions";
export function* getTimeDiffLogic() {
yield put(getTimeDiff());
const timeDiff = yield store.getState().store.timeDiff;
if (timeDiff < 250) {
yield put(handleInputEan());
yield put(resetTimeDiff());
}
}
Hope that will help someone like it helped me a lot !
Related
I'm have the title error on my application but i don't know how to solve this problem.
This is my application running:
First-image
When i click on each of these maps, a shadow is rendered on them and they are passed to a list on redux state, also a counter of how many maps you have completed it's showed.
Second-image
When you click the button awakened, the maps are re-rendered and new tier of maps are shown, and you can also click them to complete each map. Works just like the normal map logic.
First step to error
This third image it's just like the first, the difference is that i took out one map of the list, and here is where the error occurs, at this point nothing wrong happens, but the moment i click the "awakened" button the application stops and it give me that error.
I know where the error it's happening, but i didn't figure out how to solve it.
This is the component i'm working on:
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { Container } from "./styles";
import "./mapsScss";
import { mapCompleted, mapUncompleted, awakenedMapCompleted, awakenedMapUncompleted } from "./mapActions";
const base_map = require("./map_base_icon.png");
const Map = props => {
const { maps, awakenedMaps } = props;
const toggleCompletedMap = id => {
if (maps.includes(props.id)) {
props.mapUncompleted(id);
} else {
props.mapCompleted(id);
}
};
const toggleAwakenedCompletedMap = id => {
if (awakenedMaps.includes(props.id)) {
props.awakenedMapUncompleted(id);
} else {
props.awakenedMapCompleted(id);
}
};
const onClickToggle = () => {
if (props.normalActive) {
toggleCompletedMap(props.id);
}
if (props.awakenedActive) {
toggleAwakenedCompletedMap(props.id);
}
};
const baseMapRender = () => {
if (props.color_tag === "Unique") {
return;
} else {
return <img src={base_map} alt="Base Map" />;
}
};
return (
<Container id={props.id}>
<div className="map_name">{props.map_name}</div>
<div>
{baseMapRender()}
<img src={props.map_color} alt={`${props.map_name} ${props.color_tag} Map`} />
<div
className={`toggle-completed
${props.normalActive ? (maps.includes(props.id) ? "completed-map" : "") : ""}
${props.awakenedActive ? (awakenedMaps.includes(props.id) ? "completed-awakened-map" : "") : ""}
`}
onClick={onClickToggle}
></div>
</div>
<div className="map_tier">Tier {props.map_tier}</div>
</Container>
);
};
const mapStateToProps = state => ({
maps: state.map.maps,
awakenedMaps: state.map.awakenedMaps,
normalActive: state.atlas.normalActive,
awakenedActive: state.atlas.awakenedActive,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
mapCompleted,
mapUncompleted,
awakenedMapCompleted,
awakenedMapUncompleted,
},
dispatch,
);
export default connect(mapStateToProps, mapDispatchToProps)(Map);
this is the action creatos file:
export const mapCompleted = id => ({
type: "MAP_COMPLETED",
payload: id,
});
export const mapUncompleted = id => ({
type: "MAP_UNCOMPLETED",
payload: id,
});
export const awakenedMapCompleted = id => ({
type: "AWAKENED_MAP_COMPLETED",
payload: id,
});
export const awakenedMapUncompleted = id => ({
type: "AWAKENED_MAP_UNCOMPLETED",
payload: id,
});
and this is the reducers file:
const INITIAL_STATE = {
maps: [],
mapCounter: 0,
awakenedMaps: [],
awakenedMapCounter: 0,
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case "MAP_COMPLETED":
return { ...state, maps: [...state.maps, action.payload], mapCounter: state.mapCounter + 1 };
case "MAP_UNCOMPLETED":
return { maps: state.maps.filter(item => item !== action.payload), mapCounter: state.mapCounter - 1 };
case "AWAKENED_MAP_COMPLETED":
return {
...state,
awakenedMaps: [...state.awakenedMaps, action.payload],
awakenedMapCounter: state.awakenedMapCounter + 1,
};
case "AWAKENED_MAP_UNCOMPLETED":
return {
awakenedMaps: state.awakenedMaps.filter(item => item !== action.payload),
awakenedMapCounter: state.awakenedMapCounter - 1,
};
default:
return state;
}
};
There are 2 problems that need fix as I can see
Here
case "AWAKENED_MAP_UNCOMPLETED":
return {
awakenedMaps: state.awakenedMaps.filter(item => item !== action.payload),
awakenedMapCounter: state.awakenedMapCounter - 1,
};
and here
case "MAP_UNCOMPLETED":
return { maps: state.maps.filter(item => item !== action.payload), mapCounter: state.mapCounter - 1 };
you don't return the previous state, so the other properties are lost and will become undefined. This will reflect at next update of your component where you check with includes function
Try these
case "AWAKENED_MAP_UNCOMPLETED":
return {...state,
awakenedMaps: state.awakenedMaps.filter(item => item !== action.payload),
awakenedMapCounter: state.awakenedMapCounter - 1,
};
and
case "MAP_UNCOMPLETED":
return { ...state, maps: state.maps.filter(item => item !== action.payload), mapCounter: state.mapCounter - 1 };
i have such problem: I'm making To-Do-List, and now I want to make EditMode for my tasks. But when I try to do it, it returns string not an array, and that's why I have 3 errors (map,some,filter = is not a function). So I don't know how to change state(task) and return changed array.
Some details: I'm using connect to get props.
Component's code
class Item extends React.Component {
state = {
statusChange: false,
task: ''
}
activeStatusChange = () => {
this.setState( {
statusChange: true
}
);
}
deActivateStatusChange = () => {
this.setState( {
statusChange: false
}
);
this.props.editTask(this.state.task)
}
onStatusChange = (e) => {
this.setState({
task: e.currentTarget.value
})
}
render(){
return (
<div className={s.item}>
<span onClick={this.props.editStatus} className={s.statusTask}>
{this.props.status ? <img src="https://img.icons8.com/doodle/48/000000/checkmark.png"/>
: <img src="https://img.icons8.com/emoji/48/000000/red-circle-emoji.png"/>}
</span>
{ this.state.statusChange
? <input onChange={this.onStatusChange} autoFocus={true} onBlur={this.deActivateStatusChange} value={this.state.task} />
: <span className={this.props.status === true ? s.task : s.taskFalse} onClick={this.activeStatusChange}> {this.props.task} </span>}
<span onClick={this.props.deleteTask} className={s.close}><img src="https://img.icons8.com/color/48/000000/close-window.png"/></span>
</div>
)
}
}
export default Item;
Reducer's code
import React from 'react'
import shortid from 'shortid';
const ADD_TASK = 'ADD_TASK'
const EDIT_STATUS = 'EDIT_STATUS'
const TASK_DELETE = 'TASK_DELETE'
const REMOVE_ALL_DONE = 'REMOVE_ALL_DONE'
const REMOVE_ALL_TASKS = 'REMOVE_ALL_TASKS'
const EDIT_TASK = 'EDIT_TASK'
const initialState = {
tasks: []
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TASK: {
return {
...state,
tasks: [{
id: shortid.generate(),
task: action.task,
status: false
}, ...state.tasks]
}
}
case EDIT_STATUS: {
return {
...state,
tasks: state.tasks.map(task => task.id === action.id ? {...task, status: !task.status} : task)
}
}
case TASK_DELETE: {
return {
...state,
tasks: state.tasks.filter(t => t.id !== action.id)
}
}
case REMOVE_ALL_DONE: {
return {
...state,
tasks: state.tasks.filter(t => !t.status)
}
}
case REMOVE_ALL_TASKS: {
return {
...state,
tasks: []
}
}
case EDIT_TASK: {
return {
...state,
tasks: action.task
}
}
default:
return state
}
}
export const addTask = task => ({type: 'ADD_TASK', task});
export const editStatus = id => ({type: 'EDIT_STATUS', id})
export const deleteTask = id => ({type: 'TASK_DELETE', id})
export const removeAllDone = () => ({type:'REMOVE_ALL_DONE'})
export const removeAllTasks = () => ({type: 'REMOVE_ALL_TASKS'})
export const editTask = task => ({type: 'EDIT_TASK', task})
export default mainReducer;
You should create a container that uses the methods mapDispatchToProps so you can use your actions in the component.
https://react-redux.js.org/using-react-redux/connect-mapdispatch
So lets do it. Just create a file that will be your container for that component and put the code like this:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Item from 'wherever your component is';
import { addTask } from 'wherever your action is';
const mapStateToProps = ({ }) => ({
// Here you can pass the redu state to your component
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
// Here you pass the action to your component
addTask
}, dispatch)
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Item);
Then when you want to use the Item component import it from the container and it will receive in props both the action and the state that you are passing from the container file.
In you Item component you can use the action like this:
// ITem component
render() {
return (
<button onClick={this.props.addTask} />
)
}
If any doubt just let me know!
Background
I have an events page that uses JWT for authentication. I have a signout function in the header across the whole application. The basic premise, its a event display app with a many to many between users and events. Using Mongo if that matters.
I click on sign out on all pages and it works just fine. However on Landing component it throws this error.
TypeError: Cannot read property 'id' of null
Function.stateToProps [as mapToProps]
Path/client/src/components/EventActionButton.js:69
66 |
67 | const stateToProps = (state) => {
68 | return {
> 69 | userId: state.user.id
70 | }
71 | }
72 |
View compiled
▼ 19 stack frames were expanded.
mapToPropsProxy
Path/client/node_modules/react-redux/es/connect/wrapMapToProps.js:41
handleNewPropsAndNewState
Path/client/node_modules/react-redux/es/connect/selectorFactory.js:30
handleSubsequentCalls
Path/client/node_modules/react-redux/es/connect/selectorFactory.js:56
pureFinalPropsSelector
Path/client/node_modules/react-redux/es/connect/selectorFactory.js:63
runComponentSelector [as run]
Path/client/node_modules/react-redux/es/components/connectAdvanced.js:21
Connect.componentWillReceiveProps
Path/client/node_modules/react-redux/es/components/connectAdvanced.js:152
callComponentWillReceiveProps
Path/client/node_modules/react-dom/cjs/react-dom.development.js:12399
Header.js (where signout btn is)
import React , {Component} from 'react';
import {connect} from 'react-redux';
import {startSignOut} from '../actions/auth';
const Header = class Header extends Component {
handleSignOut = () => {
this.props.startSignOut();
}
render () {
return (
<div>
<span>circle</span>
<span>user</span>
<button onClick={() => this.handleSignOut()}>Sign out</button>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
startSignOut: () => startSignOut(dispatch)
});
export default connect(undefined, mapDispatchToProps)(Header);
Landing.js (Landing page at / URI)
import RequireAuth from './RequireAuth';
import {fetchEvents} from '../actions/event';
import {connect} from 'react-redux';
import EventItem from './EventItem';
import Header from './Header';
const EventDisplay = class EventDisplay extends Component {
componentDidMount = () => {
this.props.fetchEvents();
}
handleAddEvent = () => {
this.props.history.push('/addevent');
}
handleSignOut = () => {
this.props.startSignOut();
}
render() {
return (
<div>
<Header signOut={this.handleSignOut}/>
{
this.props.events.map((event, ind) => {
return <EventItem key={ind} history={this.props.history} index={ind + 1} event={event}/>
})
}
<button onClick={() => this.handleAddEvent()}>+</button>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
fetchEvents: (userId) => dispatch(fetchEvents(userId))
});
const mapStateToProps = (state) => ({
events: state.events
})
const connectedAuth = RequireAuth(connect(mapStateToProps, mapDispatchToProps)(EventDisplay));
export default connectedAuth;
EventItem.js (reusable component for each event)
import EventActionButton from './EventActionButton';
import RequireAuth from './RequireAuth';
const EventItem = (props) => {
const event = props.event;
const handleDetailView = () => {
props.history.push({
pathname: "/eventview",
state: { event: props.event }
});
}
return (
<div>
<div onClick={() => handleDetailView()}>
<span >{event.title}</span>
<span>{event.description}</span>
<span>{event.host.first_name + " " + event.host.last_name}</span>
<span>{event.date}</span>
<span>{event.capacity}</span>
</div>
<EventActionButton displayEvent={event}/>
</div>
)
}
export default RequireAuth(EventItem);
EventActionButton.js (Button in EventItem allowing users to attend or leave events)
import {connect} from 'react-redux';
import {editEventAssociation} from '../actions/event';
import RequireAuth from './RequireAuth';
const EventActionButton = class EventActionButton extends Component {
constructor(props) {
super(props);
this.state ={
edit: false,
joined: false
}
}
onClick = (e) => {
console.log(this.props);
var currentUser = this.props.userId;
if(this.state.edit) {
this.props.history.push('/addevent');
} else {
let userIds = this.props.displayEvent.users.map((user) => {
return user._id;
});
console.log(userIds);
if (this.state.joined) {
const modifiedUsers = userIds.filter((id) => id === this.props.userId);
userIds = modifiedUsers;
console.log(userIds);
} else {
userIds.push(this.props.userId);
console.log(userIds);
}
console.log("Joined " + this.state.joined);
console.log("Edited " + this.state.edit);
this.props.editEventAssociation(this.props.displayEvent._id, userIds, currentUser, this.state.joined);
}
}
componentWillMount = () => {
const event = this.props.displayEvent;
if (event.host._id === this.props.userId) {
this.setState({ edit: true, joined: false });
} else {
event.users.map((user) => {
if (user._id === this.props.userId) {
return this.setState({joined: true, edit: false});
} else {
return this.setState({join: false, edit: false});
}
});
}
}
render() {
const text = this.state.edit ? "Edit" : this.state.joined ? "Leave" : "Join";
return (
<>
<button value={text} onClick={this.onClick}>{text}</button>
</>
)
}
}
const stateToProps = (state) => {
return {
userId: state.user.id
}
}
const mapDispatchToProps = (dispatch) => {
return ({
editEventAssociation: (eventId, users, currentUser, joinedEvent) => dispatch(editEventAssociation(eventId, users, currentUser, joinedEvent))
})
}
const connectedRouterButton = connect(stateToProps, mapDispatchToProps)(EventActionButton);
export default RequireAuth(connectedRouterButton);
and just in case, this is the signout action.
export const startSignOut = (dispatch) => {
localStorage.removeItem('token');
localStorage.removeItem('user');
dispatch(removeUser());
dispatch(signOut());
}
where remove user removes currentUser in redux store and signOut removes auth token from redux store. Somehow Action button rerenders even though sign out redirects to /signin page using an HOC RequireAuth.
I dont understand how EventActionButton's re-render is triggered? Im not redirecting to Landing on /, Im redirecting to /signin
This makes sense because you're logging out so there's technically no "user" in "state.user", so instead of trying to access "state.user.id", assign a variable to "state.user" and in the constructor or wherever you're using the "id", do a check for if "state.user" is NULL and then assign the ID.
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!
all I am fairly new to React, and Redux and have been stuck with this issue for an entire day now. The data is dispatching from my component to my action creator, then to my reducer, and the state is being updated. However, when I change inputs and begin typing, it clears all other data in the form except for the data of the input I am currently typing in. If I take out the spread operator, the data then stays, but from every tutorial, I have seen this should not happen. Am I doing something wrong?
AddProject.js (form component)
import React, { useEffect } from "react";
import styles from "./AddProjects.module.css";
import { connect } from "react-redux";
import {
validateProjectId,
validateProjectDescription,
validateProjectName,
projectStartDate,
projectEndDate,
submitHandler
} from "../../Redux/createProject/action";
const AddProject = props => {
// useEffect(() => {
// console.log("aaa", props);
// }, [props]);
return (
<div className={styles.addProjectContainer}>
<h5>Create / Edit Project form</h5>
<hr />
<form>
<div>
<input
defaultValue=""
type="text"
placeholder="Project Name"
name="projectName"
style={
props.form.projectNameError
? { backgroundColor: "#F08080", opacity: "0.8" }
: { backgroundColor: "white" }
}
onChange={e => props.validateProjectName(e.target.value)}
/>
</div>
<div>
<input
type="text"
placeholder="Unique Project ID"
name="projectIdentifier"
value={props.form.projectIdentifier}
style={
props.form.projectIdentifierError
? { backgroundColor: "#F08080", opacity: "0.8" }
: { backgroundColor: "white" }
}
onChange={e => props.validateProjectId(e.target.value)}
/>
</div>
<div>
<textarea
placeholder="Project Description"
name="description"
value={props.form.description}
style={
props.form.descriptionError
? { backgroundColor: "#F08080", opacity: "0.8" }
: { backgroundColor: "white" }
}
onChange={e => props.validateProjectDescription(e.target.value)}
/>
</div>
<h6>Start Date</h6>
<div>
<input
type="date"
name="start_date"
value={props.form.start_date}
onChange={e => props.projectStartDate(e.target.value)}
/>
</div>
<h6>Estimated End Date</h6>
<div>
<input
type="date"
name="end_date"
value={props.form.end_date}
onChange={e => props.projectEndDate(e.target.value)}
/>
</div>
<button type="button" onClick={props.submitHandler}>
<span>Submit</span>
</button>
</form>
</div>
);
};
//state.form.projectName
const mapStateToProps = state => {
console.log(state.project);
return {
form: state.project
};
};
const mapDispatchToProps = dispatch => {
return {
validateProjectName: payload => dispatch(validateProjectName(payload)),
validateProjectId: payload => dispatch(validateProjectId(payload)),
validateProjectDescription: payload =>
dispatch(validateProjectDescription(payload)),
projectStartDate: payload => dispatch(projectStartDate(payload)),
projectEndDate: payload => dispatch(projectEndDate(payload)),
submitHandler: () => dispatch(submitHandler())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AddProject);
action.js (action creator)
import {
PROJECT_NAME_CHANGE,
PROJECT_IDENTIFIER_CHANGE,
PROJECT_DESCRIPTION_CHANGE,
START_DATE_CHANGE,
END_DATE_CHANGE,
SUBMIT_HANDLER,
PROJECT_NAME_ERROR,
PROJECT_IDENTIFIER_ERROR,
PROJECT_DESCRIPTION_ERROR
} from "./constants";
export const projectNameChange = projectName => {
return {
type: PROJECT_NAME_CHANGE,
projectName
};
};
export const projectNameError = () => {
return {
type: PROJECT_NAME_ERROR
};
};
export const projectIdChange = projectIdentifier => {
return {
type: PROJECT_IDENTIFIER_CHANGE,
projectIdentifier
};
};
export const projectIdError = () => {
return {
type: PROJECT_IDENTIFIER_ERROR
};
};
export const projectDescriptionChange = description => {
return {
type: PROJECT_DESCRIPTION_CHANGE,
description
};
};
export const projectDescriptionError = () => {
return {
type: PROJECT_DESCRIPTION_ERROR
};
};
export const projectStartDate = start_date => {
return {
type: START_DATE_CHANGE,
start_date
};
};
export const projectEndDate = end_date => {
return {
type: END_DATE_CHANGE,
end_date
};
};
export const submitHandler = () => {
return {
type: SUBMIT_HANDLER
};
};
export function validateProjectName(payload) {
return (dispatch, getState) => {
if (payload.length <= 30) {
dispatch(projectNameChange(payload));
} else {
dispatch(projectNameError());
}
};
}
export function validateProjectId(payload) {
return (dispatch, getState) => {
if (payload.length < 6) {
dispatch(projectIdChange(payload));
} else {
dispatch(projectIdError());
}
};
}
export function validateProjectDescription(payload) {
return (dispatch, getState) => {
if (payload.length < 256) {
dispatch(projectDescriptionChange(payload));
} else {
dispatch(projectDescriptionError());
}
};
}
// thunk call passed project name
// validateProjectName(name){
// if(name.length>4 && ){
// dispatchEvent(setName)
// }
// else{
// dispatch(setNameError)
// }
// }
index.js (Reducer)
PROJECT_NAME_CHANGE,
PROJECT_IDENTIFIER_CHANGE,
PROJECT_DESCRIPTION_CHANGE,
START_DATE_CHANGE,
END_DATE_CHANGE,
SUBMIT_HANDLER,
PROJECT_NAME_ERROR,
PROJECT_IDENTIFIER_ERROR,
PROJECT_DESCRIPTION_ERROR
} from "./constants";
const initialState = {
projectName: "",
projectIdentifier: "",
description: "",
start_date: "",
end_date: "",
projectNameError: false,
projectIdentifierError: false,
descriptionError: false
};
const createProjectReducer = (state = initialState, action) => {
switch (action.type) {
case PROJECT_NAME_CHANGE:
// console.log("We changed project name!", state.projectName, action);
return {
...state,
projectName: action.projectName
};
case PROJECT_IDENTIFIER_CHANGE:
// console.log("We changed project id!", state, action.projectIdentifier);
return {
...state,
projectIdentifier: action.projectIdentifier,
projectIdentifierError: false
};
case PROJECT_DESCRIPTION_CHANGE:
// console.log("We changed project description", state, action.description);
return { ...state, description: action.description };
case START_DATE_CHANGE:
// console.log("We changed the start date", state, action.payload);
return { ...state, start_date: action.payload };
case END_DATE_CHANGE:
// console.log("We changed the end date", state, action.payload);
return { ...state, end_date: action.payload };
case PROJECT_NAME_ERROR:
// console.log("There was an error with the project name!", state);
return { ...state, projectNameError: true };
case PROJECT_IDENTIFIER_ERROR:
// console.log("There was an error with the project Id!", state);
return { projectIdentifierError: true };
case PROJECT_DESCRIPTION_ERROR:
// console.log("There was an error with the project description!", state);
return { ...state, descriptionError: true };
case SUBMIT_HANDLER:
console.log("We submitted yayy", state);
return initialState;
//const formData = state;
//console.log(formData);
default:
return state;
}
};
export default createProjectReducer;
constants.js
export const PROJECT_IDENTIFIER_CHANGE = "PROJECT_IDENTIFIER_CHANGE";
export const PROJECT_DESCRIPTION_CHANGE = "PROJECT_DESCRIPTION_CHANGE";
export const START_DATE_CHANGE = "START_DATE_CHANGE";
export const END_DATE_CHANGE = "END_DATE_CHANGE";
export const SUBMIT_HANDLER = "SUBMIT_HANDLER";
export const PROJECT_NAME_ERROR = "PROJECT_NAME_ERROR";
export const PROJECT_IDENTIFIER_ERROR = "PROJECT_IDENTIFIER_ERROR";
export const PROJECT_DESCRIPTION_ERROR = "PROJECT_DESCRIPTION_ERROR";
rootReducer.js
const rootReducer = (state = {}, action) => {
return {
project: createProjectReducer(state.createProject, action)
};
};
export default rootReducer;
index.js (store creator)
import ReactDOM from "react-dom";
import { createStore, compose, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunkMiddleware from "redux-thunk";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import rootReducer from "./Redux/rootReducer";
const composeEnhancers =
(typeof window !== "undefined" &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunkMiddleware))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
In redux reducer you should always return the entire new version of the state, not just the updated parameter
exemple
return Object.assign({},prevState,{field: action.value});
(We don't see your reducer in your post but I guess that this is the problem)
Good documentation here - https://redux.js.org/basics/reducers/
Using redux to manage the state of a component is very bad practice. What you should do instead is to use useState to save the state of each of your inputs and control them with the onChange
Redux should be used ONLY for variables which are UI related and which have to be transported between multiple components all around the website.
https://reactjs.org/docs/hooks-state.html for more information
You need to know that everytime you update your redux store, the store is first copied (fully) then the new values are added and then the current store is replaced by the new one. Which drives to a lot of performance issues and memory leak.
The error is in your store creation
project: createProjectReducer(state.createProject, action)
should be
project: createProjectReducer(state.project, action)
The state is lost by not being passed to the sub reducer