So, I want to directly replace the value of priority which is initially 1 to a different value. I created a redux function for it, but it's sending the function more than once.
Action
export const editPriority = (id, listID, newPriority) => {
return {
type: TRELLO_CONSTANTS.PRIORITY,
payload: { id, listID, newPriority },
};
};
const { id, newPriority } = action.payload;
const card = state[id];
card.priority = newPriority;
return { ...state, [`card-${id}`]: card };
}
export const TRELLO_CONSTANTS = {
PRIORITY: 'PRIORITY',
};
and here's my function -
import {editPriority} from './actionTypes.js';
const card=({dispatch})=> {
const [newPriority, priority] = useState();
}
const changePriority = (e) => {
// newPriority(e);
priority(e);
dispatch(editPriority(id, listID, priority));
console.log(priority);
};
// and the main function
<button onClick={() => changePriority(1)}>1</button>
<button onClick={() => changePriority(2)}>0</button>
{priority === 0 ? <p>0</p> : null}
{priority === 1 ? <p>1</p> : null}
So whenever, I click the button, it only dispatches id and listID, not the priority. Plus I'm also not able to make the last 2 ternary operators work. Is this a wrong way to access them?
This is the output I get in redux extension -
{
type: 'PRIORITY',
payload: {
id: 'card-0',
listID: 'list-0'
}
}
You need to pass e directly to editPriority, since state updates are asynchronous in nature, and the change caused by priority(e) won't be reflected immediately.
const changePriority = (e) => {
// newPriority(e);
priority(e);
dispatch(editPriority(id, listID, e));
};
Suggestion:
In reducer, do not mutate the original state directly
const { id, newPriority } = action.payload;
const card = {...state[id]};
card.priority = newPriority;
return { ...state, [`card-${id}`]: card };
Also your priority state name is newPriority
So, use that value in render:
{newPriority === 0 ? <p>0</p> : null}
{newPriority === 1 ? <p>1</p> : null}
Suggestion: While naming your states you can use it as const [priority, setPriority] = useState(); to avoid the confustion
Related
I can't get reducer to return updated state. It returns the updated state with correct info on 2nd time button click. workflow is the state which has tasks: [], taskId: '' and hasError variable. On button click I call onUnassign dispatch action which returns an error to invoke WORKFLOW_VALIDATION_DATA_ERROR reducer action which updates state variable hasError to true but hasError displays true on 2nd button click. I'm not able to figure it out why? Below is my code:
const DisActionConnected: React.FC = () => {
const dispatch = useDispatch();
const {tasks, workflow} = useSelector((state: IRootState) => state.workflow);
const onUnassign = () => {
if (tasks && tasks.length > 0) {
dispatch(setUnassign(name));
console.log(workflow) // here hasError is true on 2nd time not first time
}
};
return (
<div>
<DisActions
userId={userId}
onUnassign={onUnassign}
/>
</div>
);
};
store action
export const setUnassign = (name: string) => {
return {
type: TASK_UNASSIGN_PENDING,
payload: {
name
}
}
};
reducer function
export function workflowReducer(
state: IWorkflowState = initialState,
action: WorkflowActionsType
): IWorkflowState {
switch(action.type) {
case WORKFLOW_VALIDATION_DATA_ERROR:
return {...state, hasError: true};
case TASK_UNASSIGN_PENDING:
return {...state};
default:
return {...state};
};
}
STATE
export interface IWorkflowState {
tasks: ITask[];
taskId: string;
hasError: boolean | null
};
const initialState: IWorkflowState = {
tasks: [],
taskId: '',
hasError: false
};
export function* setUnassignDis() {
const errorMessage = "error saving data"
yield put({
type: WORKFLOW_VALIDATION_DATA_ERROR,
payload: {errorMessage}
});
}
function* workflowSaga() {
yield takeEvery(WorkflowStore.TASK_UNASSIGN_PENDING, setUnassignDis);
}
React state updates are not instantaneous and this is how Javascript closures work. workflow can never be anything but what the workflow state value is when onUnassign is called. The current workflow value is closed over in callback scope. When the button is clicked again and the onUnassign callback is called it closes over the updated state from the previous update.
If you want to log updated state values then use the useEffect hook with appropriate dependency.
Example:
const {
tasks,
workflow
} = useSelector((state: IRootState) => state.workflow);
// Log when workflow state updates
React.useEffect(() => {
console.log(workflow);
}, [workflow]);
const onUnassign = () => {
if (tasks?.length) {
dispatch(setUnassign(name));
}
};
...
I'm using React Redux and want to be able to change the title and description of a post, using the onChange method. When only using React the way you would do this is that you keep an useState which you change whenever a change occurs, but I can't seem to get it to work with using redux in react. Instead of the state changing the original title, and description remains and cannot be changed.
From what I have read the basic idea is to have a listener on the input (onChange, usually) and have that fire a redux action. You then have the action tell the reducer to make the change to the store.
I have tried doing this, but could make it work correctly. What am I doing wrong and how do you solve it? I'm also wondering how do I specify that I want to change either title or description when using onChange, or do I simply send everything in post each time a change occurs?
This is what the redux state looks like when entering a post:
{
auth: {
isSignedIn: true,
user: {
id: '624481f22566374c138cf974',
username: 'obiwan',}
},
posts: {
'62448632b87b223847eaafde': {
_id: '62448632b87b223847eaafde',
title: 'hellothere',
desc: 'its been a long time since I heard that name...',
username: 'vorbrodt',
email: 'example#gmail.com',
categories: [],
createdAt: '2022-03-30T16:32:50.158Z',
updatedAt: '2022-03-30T16:32:50.158Z',
__v: 0
}
},
}
Here is where the onChange happens.
Post.js
import { getPostById, editPost } from "../actions";
const Post = ({ getPostById, editPost, username }) => {
const [updateMode, setUpdateMode] = useState(false);
let { id } = useParams();
let post = useSelector((state) => state.posts[id]);
const handleInputChange = (e) => {
try {
editPost(e.target.value);
} catch (err) {}
};
return (
<div className="post">
<div className="post-wrapper">
{updateMode ? (
<input
type="text"
value={post.title}
className="post-title-input"
autoFocus
onChange={(e) => handleInputChange(e)}
/>
) : (
<h1 className="post-title">
{post.title}
</h1>
)}
<div className="desc-area">
{updateMode ? (
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e)}
/>
) : (
<p className="post-desc">{post.desc}</p>
)}
</div>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return { username: state.auth.user.username };
};
export default connect(mapStateToProps, { getPostById, editPost })(Post);
Here is the action creator:
//edit post in redux state
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
And here is the reducer which is suppose to change the state.
postReducer.js
import _ from "lodash";
import { GET_POSTS, GET_POST, CREATE_POST, EDIT_POST } from "../actions/types";
function postReducer(state = {}, action) {
switch (action.type) {
case GET_POSTS:
return { ...state, ..._.mapKeys(action.payload, "_id") };
case GET_POST:
return { ...state, [action.payload._id]: action.payload };
case CREATE_POST:
return { ...state, [action.payload._id]: action.payload };
case EDIT_POST:
//here the change should occur, not sure how to specify if title or desc should
//change
return { ...state, [action.payload._id]: action.payload };
default:
return state;
}
}
export default postReducer;
Hey there something like this should be of help
const handleInputChange = (e, key, id) => {
try {
editPost({ [key]: e.target.value, id });
} catch (err) {}
};
Usage
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e, "title", post.id)}
/>
action
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
Reducer
case EDIT_POST:
//here we destructure the id and return the data without the id cause we //need it below
const {id, ...newData} = action.payload
const indexToUpdate = state.posts.find(post => post.id === id)
const newPostsData = [...state.posts]
//Here we update the actual object and its property that is in the state at //the specific value
newPostsData[indexToUpdate] = {...newPostData[indexToUpdate], {...newData}
return { ...state, posts: newPostsData};
I'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 a Chat component which uses API to populate the messages state, also there are different areas that have different chats which I pass as props to the component.
In this component I have 3 useEffects but I am interested in two of them which don't work properly. In the first useEffect I have some code that basically resets the messages state on area change to undefined. I need to do this to be able to distinguish between the API not being called yet where I display a loading component <Spinner /> or if the API has been called and it has retrieved an empty array to show the <NoData> component.
The problem that I have is that when I change areas the useEffects get triggered as they should but the first useEffect doesn't update the messages state to undefined before the second useEffect is called. And after a rerender because of history push the messages come as undefined but then the second useEffect doesn't get triggered anymore. I don't get why the state is not being updated in the first useEffect before the second. Also the weird thing is this used to work for me before now it doesn't. I changed some stuff up without pushing to git and now I am puzzeled. Code below:
export default function ChatPage({ history, match, area, ...props }) {
const [templates, setTemplates] = useState([]);
const [advisors, setAdvisors] = useState([]);
const [messages, setMessages] = useState(undefined);
const [conversation, setConversation] = useState([]);
const [chatToLoad, setChatToLoad] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [linkOrigin, setLinkOrigin] = useState("");
const [headerText, setHeaderText] = useState("");
// useEffect used to reset messages and conversation state
// triggered on area change(messages and conversation reset)
// and customer ID change(conversation reset).
// Required to distinguish between API call not being made yet
// and API returning no data.
useEffect(() => {
if (match.params.id) {
setLinkOrigin(match.params.id);
}
if (messages) {
if (match.params.id && messages.length !== 0) {
let matches = messages.filter(
(message) => message.ORIGINATOR === match.params.id
);
if (matches.length !== 0 && match.params.id === linkOrigin) {
setMessages(undefined);
history.push("/chats/" + match.params.area);
}
}
}
setConversation([]);
}, [area, match.params.id]);
// API calls
useEffect(() => {
if (templates.length === 0) {
api.getTemplates().then((templates) => {
setTemplates(templates);
});
}
if (advisors.length === 0) {
api.getAgents().then((advisors) => {
setAdvisors(advisors);
});
}
if (!messages || messages.length === 0) {
chooseQueue(match.params.area).then((queuesData) => {
let queues = queuesData.data.map((message) => ({
DATE_SORT: message.DATE_RECIEVED,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: "SMS_OUTBOUND",
ASSIGNED_TO: message.ASSIGNED_TO || null,
}));
setMessages(orderMessagesByDate(queues));
setChatToLoad(queues[0]);
});
}
}, [area]);
useEffect(() => {
if (messages) {
if (messages.length) {
let loadId = match.params.id ? match.params.id : messages[0].ORIGINATOR;
const params = {
MobileNumber: loadId,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: message.MSG_TYPE2.replace("MobileOriginated", "SMS"),
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
setChatToLoad(
messages.find((message) => message.ORIGINATOR === loadId)
);
history.push("/chats/" + match.params.area + "/" + loadId);
}
}
}, [messages]);
function chooseQueue(queueType) {
switch (queueType) {
case "myqueue":
setHeaderText("My chats");
return queuesApi.getMyActiveQueues(area);
case "mycompleted":
setHeaderText("My completed chats");
return queuesApi.getMyCompletedQueues();
case "queues":
setHeaderText("Chats");
return queuesApi.getQueues(area);
case "completed":
setHeaderText("Completed chats");
return queuesApi.getCompletedQueues();
default:
setHeaderText("My chats");
return queuesApi.getQueues(area);
}
}
function classifyMessage(message) {
return message.MSG_TYPE.includes("OUTBOUND") ||
message.MSG_TYPE.includes("FAULT_TEST")
? "outbound"
: "inbound";
}
async function submitMessage(message) {
var params = {
number: message.ORIGINATOR,
message: message.MESSAGE,
smssize: message.MESSAGE.length
};
await messagingApi.replyToCustomer(params).then((res) => {
if (res.data[0].RVALUE === "200") {
let extendedMsg = [...messages, message];
let extendedConversation = [...conversation, message];
setConversation([...extendedConversation]);
setMessages(orderMessagesByDate([...extendedMsg]));
}
});
}
function orderMessagesByDate(list) {
return list.sort(function(x, y) {
return new Date(y.DATE_SORT) - new Date(x.DATE_SORT);
});
}
const modalHandler = () => {
setIsOpen(!isOpen);
};
let chatConfig = {
channelSwitch: true,
channels: channels,
templateModal: true,
templates: templates,
advisorModal: true,
advisors: advisors,
};
const onActiveChatChange = (message) => {
history.push("/chats/" + match.params.area + "/" + message.ORIGINATOR);
const params = {
MobileNumber: message.ORIGINATOR,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
};
return (
<div data-test="component">
<BodyHeader
text={headerText}
children={
<FontAwesomeIcon
icon="plus-square"
aria-hidden="true"
size="2x"
onClick={modalHandler}
/>
}
/>
{messages && chatToLoad ? (
<>
<ChatWindow
messages={messages}
conversation={conversation}
chatToLoad={chatToLoad}
onActiveChatChange={onActiveChatChange}
classifyMessage={classifyMessage}
submitMessage={submitMessage}
config={chatConfig}
/>
<SendMessageModal isOpen={isOpen} toggle={modalHandler} />
</>
) : !messages ? (
<Spinner />
) : (
<NoDataHeader>There are no chats in this area</NoDataHeader>
)}
</div>
);
}
You can't get what you want this way. A state change applied in a useEffect won't have effect until the next rendering cycle, the following callbacks will still see the current const value.
If you want to change the value in the current rendering cycle the only option you have is to relax your const into let and set the variables yourself.
After all: you were expecting a const to change isn't it? ;)
I know that redux trigger react component to re-render once state of that component get changed but this doesn't happen in my situation.
Action
const addToCart = (product, cartProducts) => {
let newCartProducts = [...cartProducts, product];
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeated = acc.find(p => p.id === cur.id);
if(repeated) repeated.quantity++;
else acc.push(cur);
return acc;
}, []);
return {
type: ADD_TO_CART,
payload: newCartProducts
}
}
Reducer:
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
return action.payload;
default:
return state;
}
}
The reducer returns a new state every time the action dispatched from the component but i need to close the cart and open it again to get the effect, redux does not update the product quantity simultaneously??
You are modifying the existing elements in the state.
Use
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeatedIndex = acc.findIndex(p => p.id === cur.id);
const repeated = acc[repeatedIndex];
if (repeated) {
acc[repeatedIndex] = {
...repeated,
quantity: repeated.quantity + 1
};
} else acc.push(cur);
return acc;
}, []);
You array is recreated each time, but the objects inside it are not. So when you modify their internals you need to notify that the specific object has changed.
Refactor logic to the reducer and set the quantity here:
const addToCart = product => {
return {
type: ADD_TO_CART,
payload: product,
};
};
//I assume state is an array of products on your cart
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
const { id } = action.payload;
return state.map(p => p.id).includes(id)
? //product is already on card add quanity
state.map(p =>
p.id === id
? { ...p, quantity: p.quantity + 1 }
: p
)
: state.concat({ ...action.payload, quantity: 1 }); // add product
default:
return state;
}
};