I'm using use-undoable-reducer package from NPM for undo feature in my react app.
I can't seem to have the state updated with this package when triggering undo / redo with this package or any other undo package I found online.
My dispatch reducer action "ASSIGN" / "REMOVE" is able to update the states, but the reducer action "undo" / "redo" did update behind the hood but does not re-render.
Everything I update the code and save, the hot reload will make the undo/redo to function.
Not sure what's the problem. Would be glad to know why is this happening.
Here's my code
const newSlot = [
new Array(11).fill(null),
new Array(11).fill(null),
new Array(11).fill(null),
new Array(11).fill(null),
new Array(11).fill(null),
];
type State = {
id: string;
full_name: string;
short_name: string;
vocation: string;
rank: string;
color_code: string;
} | null;
type Action =
| {
type: "ASSIGN";
payload: { col_index: number; row_index: number; user: State };
}
| {
type: "OVERRIDE";
payload: State[][];
}
| { type: "REMOVE"; payload: { col_index: number; row_index: number } }
| { type: UndoableHistoryTypes.UNDO } | { type: UndoableHistoryTypes.REDO };
const reducer = (state: State[][], action: Action): State[][] => {
switch (action.type) {
case "ASSIGN":
let assignState = [...state];
assignState[action.payload.col_index][action.payload.row_index] =
action.payload.user;
return assignState
case "REMOVE":
let removeState = [...state];
removeState[action.payload.col_index][action.payload.row_index] = null;
return removeState
case "OVERRIDE":
let overrideState = action.payload;
return overrideState
default:
return state
}
};
const {
state,
dispatch,
canRedo,
canUndo,
triggerRedo,
triggerUndo,
} = useUndoableReducer(reducer, newSlot);
<Button size="sm" variant="secondary" onClick={() => {
triggerUndo()
console.log(state);
}} disabled={!canUndo}>
UNDO
</Button>
Related
I understand that in Strict Mode, the reducer should run twice. However, it shouldn't actually update the values twice.
The quantity for items gets updated twice.
For example, there is an item, items: [{name: tshirt, price: 10, quantity: 1}] already in the cart. If call addItem(state, tshirt, 1), the cart will update to items: [{name: tshirt, price: 10, quantity: 3}].
Only the quantity value inside the items array is updated twice. The outside variables such as value and total_qty only update once.
How do I stop it updating twice without turning off StrictMode?
interface Product {
name: string,
materials: string[],
categories: string[],
price: number,
image?: string
}
interface ICartItem extends Product {
quantity: number
}
interface Cart {
items: {[key: string]: ICartItem},
value: number,
total_qty: number
}
const addItem = (state: Cart, product: Product, quantity: number) => {
let item = state?.items?.[product.name];
if (item) {
item.quantity += quantity;
} else {
item = {
...product,
quantity
}
}
let updatedCart = {
...state,
items: {
...state.items,
[product.name]: item
},
value: Math.max(0, state.value + (product.price * quantity)),
total_qty: Math.max(0, state.total_qty + quantity)
}
return updatedCart;
}
const cartReducer: Reducer<Cart, UpdateCartAction> = (state: Cart, action: UpdateCartAction) => {
switch (action.type) {
case 'ADD_ITEM':
return addItem(state, action.product, action.quantity);
case 'REMOVE_ITEM':
return removeItem(state, action.product, action.quantity);
case 'CLEAR_CART':
return clearCart();
default:
return state;
}
}
export const CartContext = React.createContext<ICartContext | undefined>(undefined);
export const CartProvider = ({children}: {children: ReactNode}) => {
const {cart, dispatch} = useLocalStorageReducer(
'cart',
cartReducer,
initialCart
);
const contextValue = useMemo(()=>{
return {cart, dispatch}
}, [cart]);
return (
<CartContext.Provider value={contextValue}>{children}</CartContext.Provider>
)
}
export const useCart = () => {
const contextValue = useContext(CartContext);
let cart: Cart | undefined, dispatch: Dispatch<any> | undefined;
if (contextValue) {
cart = contextValue.cart;
dispatch = contextValue.dispatch;
}
const addItem = (product: Product, quantity: number) => {
if (dispatch) dispatch({type: "ADD_ITEM", product, quantity});
}
return {
cart,
addItem
}
}
I can't seem to figure out what's causing the above issue, and debug properly. From my understanding of Redux Slices, I'm able to directly mutate state in my reducer due to the Immer functionality built-in. If I hard code the redux JSON into the UI component there are no issues which leads me to believe it's a Redux issue. Any advice would be appreciated.
Slice.ts
interface LoadSchedulerState {
gridData: DataRow[] | null,
}
interface DataRow {
id: number,
dis: string,
hour: string
}
const initialState: LoadSchedulerState = {
gridData: null,
}
export const loadSchedulerSlice = createSlice({
name: 'load_scheduler',
initialState,
reducers: {
updateGridData: (state, action: PayloadAction<DataRow>) => {
let newData = [{...action.payload}]
return {...state, gridData:newData}
},
},
});
export const {updateGridData} = loadSchedulerSlice.actions;
export const gridData = (state: { loadScheduler: { gridData: any; }; }) => state.loadScheduler.gridData;
export default loadSchedulerSlice.reducer;
LoadScheduler.ts
import { AgGridColumn, AgGridReact } from "#ag-grid-community/react";
import HeaderGroupComponent from "./HeaderGroupComponent.jsx";
import LoadHeaderComponent from "./LoadHeaderComponent.jsx";
import BtnCellRenderer from './BtnCellRenderer';
import {
AllModules,
ColumnApi,
GridApi,
GridReadyEvent,
} from "#ag-grid-enterprise/all-modules";
import "../../styles/DemoGrid.css";
import { updateGridData, gridData } from "./loadSchedulerSlice";
import { useDispatch, useSelector } from 'react-redux';
const LoadSchedulerGrid = () => {
const [gridApi, setGridApi] = useState<GridApi>();
const [columnApi, setColumnApi] = useState<ColumnApi>();
const [rowData, setRowData] = useState<any>(null);
const gridStateData = useSelector(gridData);
const dispatch = useDispatch();
// PUSH TABLE CHANGES VIA WEBSOCKET TO BACKEND
const handleCellChange = (event: any) => {
}
var init_data = {
id: 0,
dis: "Mon 10/19 8:09 A",
hour: "8 a"
}
const dataSetter = (params: { newValue: any; data: any; }) => {
params.data.dis = params.newValue;
return false;
};
const onGridReady = (params: GridReadyEvent) => {
dispatch(updateGridData(init_data))
setGridApi(params.api);
setColumnApi(params.columnApi);
};
return (
<div className="ag-theme-alpine demo-grid-wrap">
<AgGridReact
onGridReady={(params) => {
onGridReady(params);
}}
immutableData={true}
rowData={gridStateData}
getRowNodeId={node => node.id}
modules={AllModules}
onCellValueChanged={handleCellChange}
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
headerComponentFramework: LoadHeaderComponent,
headerComponentParams: {
menuIcon: "fa-bars",
},
}}
>
<AgGridColumn headerName="#" width={50} checkboxSelection sortable={false} suppressMenu filter={false} pinned></AgGridColumn>
<AgGridColumn headerName="Load Details" headerGroupComponentFramework={HeaderGroupComponent}>
<AgGridColumn field="dis" width={110} headerName="Dispatch" editable cellClass="dispatch" valueSetter={dataSetter} />
<AgGridColumn field="hour" width={50} headerName="Hour" cellClass="hour" />
</AgGridColumn>
</AgGridReact>
</div>
);
};
const rules = {
dc_rules:{
"cell-blue": (params: { value: string }) => params.value === 'ERD',
"cell-beige": (params: {value: string }) => params.value === 'PDC',
"cell-cyan": (params: {value: string }) => params.value === 'CRD'
},
nr_cube_rules:{
"cell-red": (params: {value: number }) => params.value > 10.0
}
}
export default LoadSchedulerGrid;
Ag-grid per default tries to directly mutate the state object outside of a reducer. You have to use Ag-Grids immutableData setting.
https://www.ag-grid.com/javascript-data-grid/immutable-data/
They even have a blog article about using RTK with Ag-Grid (even if they use immutable logic in the reducers - within the RTK reducers this is not necessary as you correctly noted): https://blog.ag-grid.com/adding-removing-rows-columns-ag-grid-with-react-redux-toolkit/
render() {
const listItems = this.props.todos.map((todo) =>
<ListItem key={todo.id} id={todo.id} content={todo.content} onEdit={this.onEditItem}/>
)
return <>
<ul className="todo-list">
{listItems}
</ul>
{/* <AddItem/> */}
<div className="add-item">
<input type="text" onChange={this.onChangeValue}/>
<button type="submit" onClick={this.onAddItem}>Add Item</button>
</div>
</>
}
onAddItem = () => {
this.props.submitNewTodo({ id: this.props.todos.length + 1, content: this.state.value})
};
When I console.log this.props.todos.length it returns the value 2 and this.state.value returns the value typed into the input. But the "Add Item" button doesn't work.
I have mapped submitNewTodo to dispatch addTodo(newTodo) like so
const mapDispatchToProps = (dispatch) => {
return {
submitNewTodo: function(newTodo) {
dispatch(addTodo(newTodo));
}
}
}
Complete code is in this codepen.
https://codepen.io/blenderous/pen/MWjdyoN?editors=0011
Your addTodo action creator is wrong:
const addTodo = (todo) => {
type: 'ADD_TODO',
todo
};
this is a method that treats
type: 'ADD_TODO',
todo
as a method body. (type being used as the break label for the string 'ADD_TODO', followed by todo)
If you want to return an action, these two notations are correct:
const addTodo = (todo) => {
return {
type: 'ADD_TODO',
todo
}
};
// note the parantheses!
const addTodo = (todo) => ({
type: 'ADD_TODO',
todo
});
The first thing I notice with your code is that your reducer is not following the pattern Redux uses.
const todoReducer = ( state = [{ id: 1, content: "Call Client" },
{ id: 2, content: "Write Log" }], action ) => {
if (action.type == ADD_TODO) {
return state.concat(action.todo);
}
else {
return state;
}
}
The first rule that it breaks is that this should be a switch, not an if statement.
switch (action.type) {
case 'ADD_TODO':
// create new todos with the added todo
const newTodos = [
...state.todos,
action.payload.todo,
]
// new state object
return {
...state,
todos: newTodos,
}
default:
return {
...state,
}
}
The second rule is that you want to always have a payload property to follow the proper flux patterns. That payload would contain all of your data.
const addTodo = (todo) => {
type: 'ADD_TODO',
payload: {
todo,
}
};
I started working with reactjs recently and I need to know how to properly update my state.
My actions.js:
export function updateCreateCampaignObject(data) {
return {
type: actions.UPDATE_CREATE_CAMPAIGN,
payload: data,
}
}
export function updateCampaignProducts(data) {
return {
type: actions.UPDATE_CAMPAIGN_PRODUCTS,
payload: data,
}
}
export function updateCampaignTarget(data) {
return {
type: actions.UPDATE_CAMPAIGN_TARGET,
payload: data,
}
My reducer:
const INITIAL_STATE = {
campaign_dates: {
dt_start: '',
dt_end: '',
},
campaign_target: {
target_number: '',
gender: '',
age_level: {
age_start: '',
age_end: '',
},
interest_area: [],
geolocation: {},
},
campaign_products: {
survey: {
name: '',
id_product: '',
quantity: '',
price: '',
}
}
}
export default function createCampaignReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case UPDATE_CREATE_CAMPAIGN:
return { ...state, state: action.payload }
case UPDATE_CAMPAIGN_PRODUCTS:
return { ...state, campaign_products: action.payload }
case UPDATE_CAMPAIGN_TARGET:
return { ...state, campaign_products: action.payload }
default:
return state
}
In this case, I only want to add +1 to quantity in my campaign_products object.
Do I need to create an action just for this?
How can I call this action in my component, something like this?
import { updateCampaignProducts as updateCampaignProductsAction }
from '~/store/modules/createCampaign/actions'
function addQuantity() {
dispatch(updateCampaignProductsAction({
survey: {
quantity: quantity + 1
}
}))
}
Have not tested, But you need to change something like this. (3 steps)
1) Change quantity to number
campaign_products: {
survey: {
name: '',
id_product: '',
quantity: 0,
price: '',
}
}
2) In Reducer, based on action update the state from current state. (in your case not depend on any action payload)
export default function createCampaignReducer(state = INITIAL_STATE, action) { switch (action.type) {
case UPDATE_CAMPAIGN_PRODUCTS:
const current_quantity = state.campaign_products.survey.quantity;
return { ...state, campaign_products: current_quantity + 1}
default:
return state }
3) dispatch action
function addQuantity() {
dispatch(updateCampaignProductsAction())
}
you can do it with or without hooks.
both ways, read the docs about connecting redux to react.
react team wrote npm library to connent them.
I am new to the react-redux. Here, I have a reducer which is like,
const initialState = {
Low: [
{
id: 0,
technologyId: 0,
technology: '',
type: '',
count: '',
allowded: 6,
level: 'EASY'
}
],
Medium: [
{
id: 0,
technologyId: 0,
technology: '',
type: '',
count: '',
allowded: 7,
level: 'MEDIUM'
}
],
High: [
{
id: 0,
technologyId: 0,
technology: '',
type: '',
count: '',
allowded: 7,
level: 'TOUGH'
}
]
}
export default function QuizData(state = initialState, action) {
switch (action.type) {
case QUIZ_DATA:
return {
...state,
[action.data.type]: [...action.data.tobeData],
error: false,
}
case ADD_NEW:
return {
...state,
[action.data.addtype]: action.data.addData,
error: false,
}
case REMOVE_TECH:
return {
...state,
[action.data.removeType]: action.data.newArr,
error: false,
}
case RESET_QUIZ:
return {
...initialState,
error: false,
}
}
Now, Here on click of button I am calling an action that will reset the data to initial state.
this.props.resetQuiz();
which is
export function resetQuiz() {
return {
type: RESET_QUIZ
}
}
where I use it
let newData = { ...this.props.data }; while using it in the component to do some operation.
Now here what happens is after doing some actions the initial state data gets changes with some new values,..
But,on click of the button I want to set it like, initialState.
So, when I tried that time, that initialState is also getting the same values. So, It is not resetting.
I am using it in component like,
data: state.QuizData // In statetoprops.
let criterias = [{
type: 'Low',
noc: 6,
id: 1,
data: this.props.data["Low"]
}, {
type: 'Medium',
noc: 7,
id: 2,
data: this.props.data["Medium"]
},
{
type: 'High',
noc: 7,
id: 3,
data: this.props.data["High"]
}]
While using the action in component like,
createQuestionBankQuiz = () => {
this.props.resetQuiz();
history.push({
pathname: "/quiz-setup/" + `${this.props.jdId}`
});
};
export default connect(mapStateToProps, { fetchListOfQuiz, updateQuestionViewMode, enableJob, resetQuiz })(LandingScreen);
The way I update is
onChange(event, tobeupdated, id, type, noc, data) {
let newData = { ...this.props.data };
let errorState;
let isDuplicate;
let addedRow;
if (newData) {
let data = newData[type].map((object, index) => {
if (object.id === id) {
object[tobeupdated] = event.target.value;
const tobeData = newData[type];
this.props.updateLowLevel({ tobeData, type }).then(() => {
let criteria_filled = this.disableAddbutton({ ...this.props.data }, type);
addedRow = `new${type}RowAdded`;
this.setState({
[addedRow]: criteria_filled ? true : false
})
const tobechecked = newData[type].filter(item => item.id === id);
isDuplicate = this.checkPreviousSelected(newData, type, tobechecked[0].technology, tobechecked[0].type);
if (isDuplicate) {
toastr.error("Duplicate criteria. Please change it.");
object["technology"] = '';
object["type"] = '';
const tobeData = newData[type];
this.props.updateLowLevel({ tobeData, type });
}
});
}
});
errorState = `show${type}Error`;
if (tobeupdated !== "count") {
this.getSelectionQuestionsNumber(type, id, noc);
}
let validateData = this.validate(type, noc);
this.setState({
[errorState]: validateData
})
}
}
What is it that I am doing wrong ?
I think you wrong with action dispatch
export function resetQuiz() {
return dispatch => {
dispatch({
type: RESET_QUIZ
})
}
}
I think better if you try with this in reducer
export default function QuizData(state = initialState, action) {
switch (action.type) {
case RESET_QUIZ:
return Object.assign({}, initialState, {error: false})
default:
return state;
}
}
you just need to return the initial value in reducer, like this for example:
export default function QuizData(state = initialState, action) {
switch (action.type) {
case RESET_QUIZ:
return initialState
default:
return state;
}
}
You forgot to return the state in your reducer for the default action :
export default function QuizData(state = initialState, action) {
switch (action.type) {
case QUIZ_DATA:
return {
...state,
[action.data.type]: [...action.data.tobeData],
error: false,
}
case ADD_NEW:
return {
...state,
[action.data.addtype]: action.data.addData,
error: false,
}
case REMOVE_TECH:
return {
...state,
[action.data.removeType]: action.data.newArr,
error: false,
}
case RESET_QUIZ:
return {
...initialState,
error: false,
}
return state; // you forgot this
}
redux issues some kind of init action when the store is initialized, so you need to return the given state in your reducers for others actions than the ones you defined yourself.
May be you are not doing deep copy in other actions.
Can you add the code of reducer for other actions?
Edit:
Quick solution for your problems is to change your reducer like below:
export default function QuizData(state = JSON.parse(JSON.stringify(initialState)), action) {
....
}
Note: JSON.parse(JSON.stringify(initialState)) works for your case but not for all the cases, and not a good solution. You have to write deep copy logic for that.
Correct/Proper solution:
You have to modify your component logic to not modify store data directly and update it through other actions.