I created a context which has a reducer that has few properties that I am keeping track of their state.
I have 2 buttons that each one sets an active tool property. I click both buttons once each time to set the the reducer property ActiveToolName.
I have an event handler that does something when one of the reducer properties is equal to a certain value.
I am printing the reducer property value every time the event handler is called. It prints it as the correct value, but then it prints previous value. What is the problem here ?? Why does the reducer value ActiveToolName keeps re rendering once with a new value and once with the previous one
Here is a chunk of the code, can someone point out my obvious mistake ?? I am new to react
import React, { useContext, useEffect, useReducer, } from "react";
import MapViewContext from "./mapview-context.js";
const eventsContext = React.createContext({
History: [],
CurrentSelection: {},
SetActiveTool: (name) => {},
ActiveToolName: "",
});
export const MyContextProvider = (props) => {
const mapViewCtx = useContext(MapViewContext);
const reducerFn = (state, action) => {
if (action.type === "SetCurrentSelection") {
let newArray = {};
newArray = { ...action.selectedFeaturesDictionary };
return {
historyArray: state.historyArray,
CurrentFeatureSelection: { ...newArray },
ActiveToolName: state.ActiveToolName
};
}
if (action.type === "ACTIVE_APP") {
let toolName = action.toolname;
switch (toolName) {
case undefined: {
return {
historyArray: state.historyArray,
CurrentFeatureSelection: { ...state.CurrentFeatureSelection },
ActiveToolName: "NULL"
};
}
case "SELECTION": {
console.log("Setting Tool To " + toolName);
return {
historyArray: state.historyArray,
CurrentFeatureSelection: {...state.CurrentFeatureSelection},
ActiveToolName: toolName
};
}
case "PANNING": {
console.log("Setting Tool To " + toolName);
return {
historyArray: state.historyArray,
CurrentFeatureSelection: {...state.CurrentFeatureSelection },
ActiveToolName: toolName
};
}
}
const [reducerTracker, dispachToolFn] = useReducer(reducerFn, {
historyArray: [],
CurrentFeatureSelection: {},
ActiveToolName: "",
});
function ActiveToolHandler(toolName){
dispachToolFn({ type: "ACTIVE_APP", toolname: toolName});
}
if (mapViewCtx.mapView !== undefined) {
mapViewCtx.mapView.on("drag", function (event) {
// In this event which handles mouse drag movement on a
//map, that is where the value keeps changing, even
// though I am setting it twice, but the console logs it
// once with the value SELECTION, and Once with PANNING
console.log(reducerTracker.ActiveToolName);
if (reducerTracker.ActiveToolName != "PANNING") {
event.stopPropagation();
}
});
}
return (
<eventsContext.Provider
value={{
History: reducerTracker.historyArray,
SetActiveTool: ActiveToolHandler
CurrentSelection: reducerTracker.CurrentFeatureSelection
ActiveToolName: reducerTracker.ActiveToolName,
}}>{props.children}
</eventsContext.Provider>
);
};
export default eventsContext;
Related
I have a form where I put a float value (1.1, 1.2, 1.9 and so on) and I want to store a bunch of them inside an array on an atom:
import { atom } from 'recoil';
export const valueState = atom({
key: 'values',
default: []
});
Whenever I write a value and it's checked that it's a double, the value gets added to valueState, however, I want to make it so if that the value I write on the form gets deleted, it also deletes the value from the valueState array. I tried by using pop, however, if I do so the program crashes. How can I do it then?
import { valueState as valueStateAtom } from '../../atoms/Atoms';
import { useSetRecoilState } from 'recoil';
const setValue = useSetRecoilState(valueStateAtom);
// The function that handles the onChange event of the form
const setNewValue = (v) => {
if (v !== '') {
const valueNumber = parseFloat(v);
if (!isNaN(valueNumber)) {
setPageValueChanged(true);
pageValue = valueNumber;
// The value gets added to valueState
setValue((prev) => prev.concat({ valueNumber, cardID }));
} else {
setPageValueChanged(false);
}
} else {
setPageValueChanged(false);
// Delete v from the atom array here
}
};
pop did not work for you because it does not return a new array (state immutability)
I think you can do a trick with filter. For example
setValue((prev) => prev.filter((value, index) => index !== prev.length - 1));
Full code
import { valueState as valueStateAtom } from '../../atoms/Atoms';
import { useSetRecoilState } from 'recoil';
const setValue = useSetRecoilState(valueStateAtom);
// The function that handles the onChange event of the form
const setNewValue = (v) => {
if (v !== '') {
const valueNumber = parseFloat(v);
if (!isNaN(valueNumber)) {
setPageValueChanged(true);
pageValue = valueNumber;
// The value gets added to valueState
setValue((prev) => prev.concat({ valueNumber, cardID }));
} else {
setPageValueChanged(false);
}
} else {
setPageValueChanged(false);
setValue((prev) => prev.filter((value, index) => index !== prev.length - 1));
}
};
One more feedback, your concat is seemingly incorrect. It's expecting to have an array param but you passed an object. The modification can be
setValue((prev) => prev.concat([{ valueNumber, cardID }]));
I have a component which when using CTRL+Z should trigger an undo action. Tracing the code through it is obvious that the state is updated correctly and that the arrays in it are not being mutated. However the component is not rerendered until I click on it, which causes a highlight to occur. At this point the component jumps to its previous location. I have attempted using forceUpdate() after the undo action is dispatched but that did not succeed either.
My reducer is a single line returning state and the new object as the action.payload. My action creator reads the original data, clones everything (some of them multiple times in a 'swing wildly' attempt to solve this) and then dispatches the undo action and data to the reducer.
Stepping through the code and comparing values shows me that everything seems correct so I cannot see where the issue is.
Here is my action creator:
export const Layout_Undo_Change = (callback) => (dispatch, getState) => {
const state = getState();
const desks = state.layout_moveData.desks;
//if no past to undo to
if (desks.past.length === 0) return;
const previous = clone(desks.past)[desks.past.length - 1];
const undoPast = clone(desks.past.slice(0, desks.past.length - 1));
const undoFuture = clone([desks.present, ...clone(desks.future)])
const undoDesks = { past: undoPast, future: undoFuture, present: previous };
dispatch({ type: ActionTypes.LAYOUT_UNDO_MOVES, payload: undoDesks });
// callback();
}
and here is the reducer:
export const layout_moveData = (state = {
desks: {
past: [],
present: null,
future: []
}
}, action) => {
switch (action.type) {
case ActionTypes.LAYOUT_DESKS_LOADING:
return { ...state, desks: { present: [], past: [], future: [] } };
case ActionTypes.LAYOUT_DESKS_LOADED:
return { ...state, desks: { present: action.payload, past: [], future: [] } };
case ActionTypes.LAYOUT_DESK_DELETED:
return { ...state, desks: action.payload };
case ActionTypes.LAYOUT_RESTORE_ALL:
return { ...state, desks: { present: [], past: [], future: [] } };
case ActionTypes.LAYOUT_SET_MOVES:
return { ...state, desks: action.payload };
case ActionTypes.LAYOUT_UNDO_MOVES:
return { ...state, desks: action.payload };
case ActionTypes.LAYOUT_REDO_MOVES:
return { ...state, desks: action.payload };
default:
return state
}
}
and finally here is the calling line from the component:
handleKeyPress = (e) => {
console.log("Layout.handleKeyPress");
if (this.state.edit) {
switch (e.code) {
case 'KeyZ':
if (e.ctrlKey) {
this.props.Layout_Undo_Change(this.forceUpdate);
e.cancelBubble = true;
this.forceUpdate();
}
break;
case 'KeyY':
if (e.ctrlKey) {
//this.props.Layout_Redo_Change();
UndoMove.redo();
e.cancelBubble = true;
}
break;
default:
break;
}
}
}
Edit - adding mapState code
mapDispatchToProps code:
const mapDispatchToProps = (dispatch) => {
//add action creators here - by reference?
return {
Layout_Set_Current_Site: (siteId) => { dispatch(Layout_Set_Current_Site(siteId)) },
Layout_Get_Sites: () => { dispatch(Layout_Get_Sites()) },
Layout_Get_Map_Background: (siteId, callback) => { dispatch(Layout_Get_Map_Background(siteId, callback)) },
Layout_Get_Desk_Types: () => { dispatch(Layout_Get_Desk_Types()) },
Layout_Fetch_Desks: (siteId) => { dispatch(Layout_Fetch_Desks(siteId)) },
Layout_Undo_Change: (callback) => { dispatch(Layout_Undo_Change(callback)) },
Layout_Redo_Change: () => { dispatch(Layout_Redo_Change()) },
Layout_Clear_Desk: (deskId) => { dispatch(Layout_Clear_Desk(deskId)) },
Layout_Delete_Desk: (deskId) => { dispatch(Layout_Delete_Desk(deskId)) },
Layout_Update_Desk_Data: (desk, deskId) => { dispatch(Layout_Update_Desk_Data(desk, deskId)) },
Layout_Get_UserImages: (deskId) => { dispatch(Layout_Get_UserImages(deskId)) },
Layout_Create_Desk: (type, siteId, height, width) => { dispatch(Layout_Create_Desk(type, siteId, height, width)) },
Layout_Restore_All: () => { dispatch(Layout_Restore_All()) },
Layout_Set_Current_Desk: (deskId) => { dispatch(Layout_Set_Current_Desk(deskId)) }
};
}
mapStateToProps code:
const mapStateToProps = (state) => {
return {
layout: state.layout,
layout_moveData: state.layout_moveData,
roles: state.siteMap.siteMapData.userRoles
}
}
Any help to point me in the correct direction would be awesome.
Extrapolated all components and items to separate classes to better handle individual state changes rather than dealing with everything from top down
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 am pretty new to react and hooks, and I'm struggling with useEffect(). I've watched all the vids and read all the docs and still can't quite wrap my head around the error I'm getting. ("onInput is not a function" when my New Article route loads). onInput points to a callback function in my form-hook.js. Why isn't it registering?
In my input.js component:
import React, { useReducer, useEffect } from 'react';
import { validate } from '../../util/validators';
import './Input.css';
const inputReducer = (state, action) => {
switch (action.type) {
case 'CHANGE':
return {
...state,
value: action.val,
isValid: validate(action.val, action.validators)
};
case 'TOUCH': {
return {
...state,
isTouched: true
}
}
default:
return state;
}
};
const Input = props => {
const [inputState, dispatch] = useReducer(inputReducer, {
value: props.initialValue || '',
isTouched: false,
isValid: props.initialValid || false
});
const { id, onInput } = props;
const { value, isValid } = inputState;
useEffect(() => {
console.log(id);
onInput(id, value, isValid)
}, [id, value, isValid, onInput]);
const changeHandler = event => {
dispatch({
type: 'CHANGE',
val: event.target.value,
validators: props.validators
});
};
const touchHandler = () => {
dispatch({
type: 'TOUCH'
});
};
//if statement to handle if you are updating an article and touch the category.... but it's valid
const element =
props.element === 'input' ? (
<input
id={props.id}
type={props.type}
placeholder={props.placeholder}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
) : (
<textarea
id={props.id}
rows={props.rows || 3}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
);
return (
<div
className={`form-control ${!inputState.isValid && inputState.isTouched &&
'form-control--invalid'}`}
>
<label htmlFor={props.id}>{props.label}</label>
{element}
{!inputState.isValid && inputState.isTouched && <p>{props.errorText}</p>}
</div>
);
};
export default Input;
useEffect(() => {onInput points to the onInput prop in NewArticle.js component where users can enter a new article.
import Input from '../../shared/components/FormElements/Input';
import { useForm } from '../../shared/hooks/form-hook';
const NewArticle = () => {
const [formState, inputHandler] = useForm({
title: {
value: '',
isValid: false
}
}, false );
return (
<Input
id="title"
element="input"
type="text"
label="Title"
onInput={inputHandler}
/> );
};
export default NewArticle;
...and then in my form-hook.js inputHandler is a callback. So, onInput points to a callback function through a prop. It was working, registering onInput as a function and then, all of a sudden it was throwing an error. What gives?
import { useCallback, useReducer } from 'react';
const formReducer = (state, action) => {
switch (action.type) {
case 'INPUT_CHANGE':
let formIsValid = true;
for (const inputId in state.inputs) {
if (!state.inputs[inputId]) {
continue;
}
if (inputId === action.inputId) {
formIsValid = formIsValid && action.isValid;
} else {
formIsValid = formIsValid && state.inputs[inputId].isValid;
}
}
return {
...state,
inputs: {
...state.inputs,
[action.inputId]: { value: action.value, isValid: action.isValid }
},
isValid: formIsValid
};
case 'SET_DATA':
return {
inputs: action.inputs,
isValid: action.formIsValid
};
default:
return state;
}
};
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity
});
const inputHandler = useCallback((id, value, isValid) => {
dispatch({
type: 'INPUT_CHANGE',
value: value,
isValid: isValid,
inputId: id
});
}, []);
const setFormData = useCallback((inputData, formValidity) => {
dispatch({
type: 'SET_DATA',
inputs: inputData,
formIsValid: formValidity
});
}, []);
return [formState, inputHandler, setFormData];
};
Thanks, ya'll.
I can give you some advice on how to restructure your code. This will ultimately solve your problem.
Maintain a single source of truth
The current state of your UI should be stored in a single location.
If the state is shared by multiple components, your best options are to use a reducer passed down by the Context API (redux), or pass down the container component's state as props to the Input component (your current strategy).
This means you should remove the Input component's inputReducer.
The onInput prop should update state in the container component, and then pass down a new inputValue to the Input component.
The DOM input element should call onInput directly instead of as a side effect.
Remove the useEffect call.
Separation of Concerns
Actions should be defined separately from the hook. Traditionally, actions are a function that returns an object which is passed to dispatch.
I am fairly certain that the useCallback calls here are hurting performance more than helping. For example inputHandler can be restructured like so:
const inputChange = (inputId, value, isValid) => ({
type: 'INPUT_CHANGE',
value,
isValid,
inputId
})
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity,
})
const inputHandler = (id, value, isValid) => dispatch(
inputChange(id, value, isValid)
)
}
Learn how to use debugger or breakpoints in the browser. You would quickly be able to diagnose your issue if you put a breakpoint inside your useEffect call.
I have a list of products called work items stored on my Redux store and I want to add an action that adds new work item or remove existing one when user picks up a a work item from the ui.
What I have so far is this workItemReducer:
import {
FETCH_WORKITEMS_BEGIN,
FETCH_WORKITEMS_SUCCESS,
FETCH_WORKITEMS_FAILURE,
SELECTED_WORKITEM
} from '../actions/workItemAction';
const initialState = {
workItems: [{"name":'work 1'}, {"name":'work 2'}, {"name":'work 3'}],
workItemsSelected: {},
loading: false,
error: null
};
export default function workItemReducer(state = initialState, action) {
switch(action.type) {
case FETCH_WORKITEMS_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_WORKITEMS_SUCCESS:
return {
...state,
loading: false,
workItems: action.payload.workItems
};
case FETCH_WORKITEMS_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
workItems: []
};
case SELECTED_WORKITEM:
return {
...state,
workItemsSelected: action.payload.workItem
};
default:
return state;
}
}
and the actions looks as below:
export const FETCH_WORKITEMS_BEGIN = 'FETCH_WORKITEMS_BEGIN';
export const FETCH_WORKITEMS_SUCCESS = 'FETCH_WORKITEMS_SUCCESS';
export const FETCH_WORKITEMS_FAILURE = 'FETCH_WORKITEMS_FAILURE';
export const SELECTED_WORKITEM = 'SELECTED_WORKITEM';
export const fetchWorkItemsBegin = () => ({
type: FETCH_WORKITEMS_BEGIN
});
export const fetchWorkItemsSuccess = workItems => ({
type: FETCH_WORKITEMS_SUCCESS,
payload: { workItems }
});
export const fetchWorkItemsFailure = error => ({
type: FETCH_WORKITEMS_FAILURE,
payload: { error }
});
export const selectedWorkItem = workItem => ({
type: SELECTED_WORKITEM,
payload: { workItem }
});
I have a container component that disptach or call these actions which I am a bit confused where the logic of adding a new one or removing existing one happens, either on the container/smart component or directly in the reducer.
Container component has this method:
onWorkItemSelect = (workItem) => {
this.props.dispatch(selectedWorkItem(workItem));
};
Anyone can help on writing the logic of adding new or remove existing one and where that code should live?
adding this to reducer works thou im not sure if all this code should remain into the reducer:
case SELECTED_WORKITEM:
let arr = [];
if (containsObject(action.payload.workItem, state.workItemsSelected)) {
arr = remove(state.workItemsSelected, action.payload.workItem);
} else {
arr = [...state.workItemsSelected, action.payload.workItem];
}
return {
...state,
workItemsSelected: arr
};
It should be done in the reducer
when adding one you could just spread the current array which you can get from the reducer state
const { workItems } = state;
const { workItem } = action.payload;
return {
// ...other stuff to return
workItems: [...workItems, workItem],
}
to delete one
const { workItems } = state;
const { workItem } = action.payload;
return {
// ...other stuff to return
workItems: workItems.filter(x => x.name === workItem.name),
}