Invalid hook call which I can't seem to fix - javascript

title pretty much says it, I've looked at some examples, but none of the fixes really worked for me. I understand it's the const, moving it within the class and out of the export, no luck. Can't seem to get it working. Anyone got any ideas? Thank you.
I get the error below at the line const [ws, setWs] = useState();
"Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app"
export default function PlayerHoc(ComposedComponent) {
const [ws, setWs] = useState();
const [roomIp, setRoomIp] = useState();
useEffect(() => {
const wsUrl =
process.env.NODE_ENV == "development"
? "ws://localhost:8888"
: "ws://" + roomIp;
setWs(new WebSocket(wsUrl));
}, [roomIp]);
useEffect(() => {
if (!ws) return;
ws.onopen = () => {
ws.send("PlaybackRequest");
};
}, [ws]);
class PlayerHoc extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.playing || (this.props.playing && !nextProps.playing);
}
componentDidUpdate(prevProps) {
if (prevProps.currentSong.id !== this.props.currentSong.id) {
const id = this.props.currentSong.id;
const other = this.props.currentSong.linked_from
? this.props.currentSong.linked_from.id
: null;
this.props.containsCurrentSong(other ? `${id},${other}` : id);
}
}
render = () => (
<ComposedComponent
{...this.props}
playContext={(context, offset) => this.props.playSong(context, offset)}
playSong={() => this.props.playSong()}
{...setRoomIp(this.props.roomIp)}
/>
);
}
//testbug
const mapStateToProps = state => {
return {
currentSong: state.playerReducer.status
? state.playerReducer.status.track_window.current_track
: {},
contains: state.libraryReducer.containsCurrent ? true : false,
trackPosition: state.playerReducer.status
? state.playerReducer.status.position
: 0,
playing: state.playerReducer.status
? !state.playerReducer.status.paused
: false
};
};
function nextSong(skip) {
ws.send(JSON.stringify({ type: "skipSong", data: skip }))
}
function previousSong(prev) {
ws.send(JSON.stringify({ type: "previousSong", data: prev }))
}
function pauseSong(pause) {
ws.send(JSON.stringify({ type: "pauseSong", data: pause }))
}
function playSong(play) {
ws.send(JSON.stringify({ type: "playSong", data: play }))
}
function seekSong(seek) {
ws.send(JSON.stringify({ type: "seekSong", data: seek }))
}
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
nextSong,
previousSong,
pauseSong,
playSong,
seekSong,
},
dispatch
);
};
return connect(mapStateToProps,mapDispatchToProps)(PlayerHoc);
}

Well, this is a mess. But let's try to refactor this into a working Higher Order Component.
There are several issues here, but the main ones are:
Defining a class component inside of a functional component
Improper use of hooks.
So lets start by defining a normal Higher Order Component. Lets call it withPlayer.
withPlayer is going to return a Class component.
Inside this class component we can do things like create a websocket, and build all of your player controls.
Then we can pass those player controls as props to the Wrapped Component.
Finally, our default export will apply our redux connect HOC. We can use the compose function from redux, to compose both withPlayer and connect onto our Wrapped Component.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
function withPlayer(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
const wsUrl =
process.env.NODE_ENV == 'development' ? 'ws://localhost:8888' : 'ws://' + props.roomIp;
this.ws = new WebSocket(wsUrl);
}
shouldComponentUpdate(nextProps) {
return nextProps.playing || (this.props.playing && !nextProps.playing);
}
componentDidUpdate(prevProps) {
if (prevProps.currentSong.id !== this.props.currentSong.id) {
const id = this.props.currentSong.id;
const other = this.props.currentSong.linked_from
? this.props.currentSong.linked_from.id
: null;
this.props.containsCurrentSong(other ? `${id},${other}` : id);
}
}
nextSong = (skip) => {
this.ws.send(JSON.stringify({ type: 'skipSong', data: skip }));
};
previousSong = (prev) => {
this.ws.send(JSON.stringify({ type: 'previousSong', data: prev }));
};
pauseSong = (pause) => {
this.ws.send(JSON.stringify({ type: 'pauseSong', data: pause }));
};
playSong = (play) => {
this.ws.send(JSON.stringify({ type: 'playSong', data: play }));
};
seekSong = (seek) => {
this.ws.send(JSON.stringify({ type: 'seekSong', data: seek }));
};
render() {
return (
<WrappedComponent
{...this.props}
playContext={(context, offset) => this.playSong(context, offset)}
nextSong={this.nextSong}
previousSong={this.previousSong}
pauseSong={this.pauseSong}
playSong={this.playSong}
seekSong={this.seekSong}
/>
);
}
};
}
const mapStateToProps = (state) => {
return {
currentSong: state.playerReducer.status
? state.playerReducer.status.track_window.current_track
: {},
contains: state.libraryReducer.containsCurrent ? true : false,
trackPosition: state.playerReducer.status ? state.playerReducer.status.position : 0,
playing: state.playerReducer.status ? !state.playerReducer.status.paused : false,
};
};
export default compose(withPlayer, connect(mapStateToProps));
This is how you would use it
import withPlayer from './withPlayer'
const MyComponent = props => {
return <>You're Player wrapped component</>
}
export default withPlayer(Mycomponent);

Related

"Unhandled Rejection (Error): Too many re-renders..." because I'm setting state within a loop?

I'm getting a "Unhandled Rejection (Error): Too many re-renders. React limits the number of renders to prevent an infinite loop." message for the following code. Not sure what is causing this issue.
I think it's because I'm calling the setNewNotifications(combineLikesCommentsNotifications) within the users.map loop. But if I move setNewNotifications(combineLikesCommentsNotifications) outside of the loop, it can no longer read likeNewNotifications / commentNewNotifications. What is the best approach to this?
Code below, for context, users returns:
const users = [
{
handle: "BEAR6",
posts: undefined,
uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1"
},
{
handle: "BEAR5",
posts: [
{
comment: false,
handle: "BEAR5",
key: "-Mmx7w7cTl-x2yGMi9uS",
like: {
Mn4QEBNhiPOUJPBCwWO: {
like_notification: false,
postId: "-Mmx7w7cTl-x2yGMi9uS",
postUserId: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1",
userLikeHandle: "BEAR6"
}},
post_date: 1635260810805,
title: "hello"
},
{
comment: false,
comments_text: {0: {
comment_date: 1635399828675,
comment_notification: false,
commenter_comment: "hi1",
commenter_handle: "BEAR6",
commenter_uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1",
key: "-Mn4QF1zT5O_pLRPqi8q"
}},
handle: "BEAR5",
key: "-MmxOs0qmFiU9gpspEPb",
like: {
Mn4QDCOrObhcefvFhwP: {
like_notification: false,
postId: "-MmxOs0qmFiU9gpspEPb",
postUserId: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1",
userLikeHandle: "BEAR6"},
Mn4QKEk95YG73qkFsWc: {
postId: "-MmxOs0qmFiU9gpspEPb",
postUserId: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
uid: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
userLikeHandle: "BEAR5"
}},
post_date: 1635265250442,
title: "hi"
}
],
uid: "rFomhOCGJFV8OcvwDGH6v9pIXIE3"
}
]
Code
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
export default function Notifications() {
const [newNotifications, setNewNotifications] = useState('')
const users = useSelector(state => state.users)
return users.map((post) => {
if(post.posts){
return post.posts.map((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null
const comments = postContent.comments_text ? Object.values(postContent.comments_text) : null
const likeNewNotifications = likes ? likes.filter(post => {
return post.like_notification === false
} ) : null
const commentNewNotifications = comments ? comments.filter(post => {
return post.comment_notification === false
} ) : null
const combineLikesCommentsNotifications = likeNewNotifications.concat(commentNewNotifications)
setNewNotifications(combineLikesCommentsNotifications)
}
)
}
return (
<div>
<p>
{newNotifications}
</p>
</div>
);
}
)
}
There are multiple errors. But lets face it step by step.
I'll copy and paste your code, but with extra comments, to let you know where I'm referencing:
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
export default function Notifications() {
const [newNotifications, setNewNotifications] = useState('')
const users = useSelector(state => state.users)
// Step 0: I guess the error is because this users.map is running everytime (with any update in the component. So, when you set a new state, it'll render again. So, you have to do this, probably 2 times: on mount and after one update.
// Step 1: You're using users.map but it returns a new array. My recommendation would be: use users.forEach instead.
return users.map((post) => {
if(post.posts){
return post.posts.map((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null
const comments = postContent.comments_text ? Object.values(postContent.comments_text) : null
const likeNewNotifications = likes ? likes.filter(post => {
return post.like_notification === false
} ) : null
const commentNewNotifications = comments ? comments.filter(post => {
return post.comment_notification === false
} ) : null
const combineLikesCommentsNotifications = likeNewNotifications.concat(commentNewNotifications)
setNewNotifications(combineLikesCommentsNotifications)
}
)
}
return (
<div>
<p>
{newNotifications}
</p>
</div>
);
}
)
}
(Read Step 0 and Step 1 as comments in the code)
Also, about:
But if I move setNewNotifications(combineLikesCommentsNotifications) outside of the loop, it can no longer read likeNewNotifications / commentNewNotifications. What is the best approach to this?
You can do
Step 3: To be able to do that, you can use let, set one variable in the parent of the loop and update the value inside the loop (or if you have an array can push even if it's const). it'd be like:
function foo() {
const users = [{}, {}, {}, {}];
const usersWithEvenId = [];
users.forEach(user => {
if (user.id % 2 === 0) {
usersWithEvenId.push(user)
}
})
}
Taking in consideration these 3 steps the resulted code would be like:
import React, { useState, useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
export default function Notifications() {
const [newNotifications, setNewNotifications] = useState('');
const users = useSelector(state => state.users);
// Function to get new posts
const getNewPosts = () => {
const notifications = [];
users.forEach((user) => {
if (user.posts) {
posts.forEach((post) => {
// Your logic;
notifications.push(newNotifications)
})
}
});
setNewNotifications(notifications);
};
// Run to get newPosts on mount (but also in any other moment)
useEffect(() => {
getNewPosts();
}, [])
return (
<div>
<p>
{newNotifications}
</p>
</div>
);
}
Maybe you can write the code like this:
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
export default function Notifications() {
const users = useSelector((state) => state.users);
const combineLikesCommentsNotifications = users.map((post) => {
if (post.posts) {
return post.posts.map((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null;
const comments = postContent.comments_text
? Object.values(postContent.comments_text)
: null;
const likeNewNotifications = likes
? likes.filter((post) => {
return post.like_notification === false;
})
: null;
const commentNewNotifications = comments
? comments.filter((post) => {
return post.comment_notification === false;
})
: null;
const combineLikesCommentsNotifications = likeNewNotifications.concat(
commentNewNotifications
);
setNewNotifications(combineLikesCommentsNotifications);
});
}else{
return [];
}
})
const [newNotifications, setNewNotifications] = useState(combineLikesCommentsNotifications);
return (
<div>
<p>{newNotifications}</p>
</div>
); ;
}

React class Component not re render after props change from redux action

The component does not re render after successfully update state in redux
i have tried to do some condition in componentShouldUpdate end up with loading true without change
reducer.js
import * as types from "./actionsType";
const INITIAL_STATE = {
slide_data: [],
error: null,
loading: false,
};
const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties,
};
};
const slideReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case types.SLIDES_FETCH_START:
return updateObject(state, {
error: null,
loading: true,
});
case types.SLIDES_FETCH_SUCSSES:
return updateObject(state, {
slide_data: action.payload,
error: null,
loading: false,
});
case types.SLIDES_FETCH_FAIL:
return updateObject(state, {
error: action.error,
loading: false,
});
default:
return state;
}
};
export default slideReducer;
actions.js
import * as types from "./actionsType";
import axios from "axios";
import { selectSlides } from "./slides.selectors";
export const slidesStart = () => {
return {
type: types.SLIDES_FETCH_START,
};
};
export const slidesSucces = (slides) => {
return {
type: types.SLIDES_FETCH_SUCSSES,
payload: slides,
};
};
export const slidesFail = (error) => {
return {
type: types.SLIDES_FETCH_FAIL,
error: error,
};
};
export const fetchSlides = () => {
return (dispatch) => {
console.log("fetch Start");
dispatch(slidesStart());
axios
.get("http://127.0.0.1:8000/slides/", {
headers: {
"Content-Type": "application/json",
},
})
.then((res) => {
dispatch(slidesSucces(res.data));
})
.catch((err) => dispatch(slidesFail(err)));
};
};
component
class IntroPage extends Component {
constructor(props) {
super(props);
this.tlitRef = React.createRef();
this.titlelRef = React.createRef();
this.subTitleRef = React.createRef();
this.showcase = React.createRef();
}
componentDidMount() {
this.props.fetchSlides();
}
render() {
const { slides, loading } = this.props;
if (loading) {
return <h1>Loading</h1>;
}
return (
<div className="intro">
<div className="wrapper">
{slides.map((data) => (
<SwiperSlides data={data} key={data.name} />
))}
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
loading: state.slides.loading,
error: state.slides.error,
slides: state.slides.slide_data,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchSlides: () => dispatch(fetchSlides()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(IntroPage);
Register the redux-logger correctly. The data was returned but nothing changes when I do redux-persist and try to reload data come through
update ::
when I change the size of the browser data it correctly appears what is this !!
update : this problem related to swiperjs
and the solution will be like that:
1 - assign swiper instance to React.CreateRef(null) : this.swiper = React.CreateRef(null)
2 - in componentDidUpdate() make a swiper update : this.swiper.current.update()
4 - use a arrow function syntax in swiper on functions to refer to the outer scope

Detect elapsed time between two conditions

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 !

Action doesn't receive the correct prop

I'm using React/Redux in this code and I'm trying to pass the correct prop by action. My intention is to change converter name on click modal button. But when I debbug, console server shows me the same action with no alteration clicking on confirm button.
My action in file actions:
export const saveOrUpdateConverter = converter => {
return {
converter,
type: CONVERTER.SAVE_OR_UPDATE_CONVERTER
};
};
The function I'm using to do that:
export const saveOrUpdateConverter = (converter, type) => {
const url = `${BASE_URL}/SaveOrUpdateConverter`;
let converterWithoutId = {
...converter,
Id: 0
};
return makeRequest(
{
method: "post",
url,
data: type === "edit" ? converter : converterWithoutId
},
(data, dispatch) => {
// if we are adding a new converter, we need to remove it from newConverters
if (type === "add") {
dispatch(actions.removeFromNewConverters(converter));
}
dispatch(actions.saveOrUpdateConverter(data));
},
true
);
};
The file where I'm calling the function
const handleSaveUpdateConverter = async () => {
let type = "edit";
return await props.saveOrUpdateConverter(converter, type);
};
Component receiving function by prop:
<AddOrEditConverterModal
converter={converter}
show={showEditConverterModal}
onCloseModal={() => setShowEditConverterModal(false)}
saveOrUpdateConverter={(converter, propsType) =>
handleSaveUpdateConverter(converter, propsType)
}
type={"edit"}
/>
I finally call the props saveOrUpdateConverter in other file:
const updateConverter = async () => {
if (converter.IntervalToSavePayload < 5) {
props.requestError(
true,
props.intl.formatMessage({
id: "modal.base.converter.interval.save.pyload.error"
})
);
return;
}
await props.saveOrUpdateConverter(converter, props.type);
debugger
props.onCloseModal();
};
Connect function to use saveOrUpdateConverter :
import { connect } from "react-redux";
import { saveOrUpdateConverter } from "Features/Devices/Converters/actions";
import ConverterPage from "./ConverterPage";
const mapStateToProps = state => ({
activeConverters: state.converter.activeConverters,
activeInstruments: state.instrument.activeInstruments
});
export default connect(mapStateToProps, {saveOrUpdateConverter})(ConverterPage);

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