I have set up a Redux slice for which I've created a custom wrapper-hook (I hate the "{ type: x, payload: y }
" syntax :p)
Here's the slice:
// polycanvas.ts (Redux)
export const polycanvas = createSlice({
name: 'polycanvas',
initialState,
reducers: {
// Arcs
addArc: (state, action: PayloadAction<Arc>) => {
if (state.arcs.length < state.settings.arc.maxArcs) {
state.arcs = [...state.arcs, action.payload];
} else {
throw(`Number of arcs exceeds the maximum. (max: ${state.settings.arc.maxArcs}, got: ${state.arcs.length})`);
}
},
removeArc: (state, action: PayloadAction<number>) => {
if (state.arcs[action.payload]) {
state.arcs = state.arcs.filter((_, index) => index != action.payload);
} else {
throw(`Tried removing arc at index ${action.payload}. No such arc exists.`);
}
},
setArcs: (state, action: PayloadAction<Arc[]>) => {
if (action.payload.length < state.settings.arc.maxArcs) {
state.arcs = action.payload;
} else {
throw(`Number of arcs exceeds the maximum. (max: ${state.settings.arc.maxArcs}, got: ${action.payload.length})`);
}
},
//... other stuff
}
});
export const usePolycanvas = () => {
const dispatch = useAppDispatch();
const arcs = useAppSelector(state => state.polycanvas.arcs);
const arc = {
addArc: (arc: Arc) =>
dispatch(polycanvas.actions.addArc(arc)),
removeArc: (index: number) =>
dispatch(polycanvas.actions.removeArc(index)),
setArcs: (arcs: Arc[]) =>
dispatch(polycanvas.actions.setArcs(arcs)),
//... other stuff
}
return { arc, arcs };
}
As far as I'm concerned, this is correct Redux usage.
In my React Component:
// Polycanvas.tsx (React Component)
export default () => {
const { arc, arcs } = usePolycanvas();
const makeArcs = (arcCount: number): void => {
const _arcs = [];
for (var i = 0; i < arcCount; i++) {
const velocity = (4 * Math.PI * (settings.speed.loops - i)) / settings.speed.timeframeSeconds;
_arcs.push({
color: { r: 255, g: 255, b: 255, a: 0.3 },
velocity,
nextImpactTime: calculateImpactTime(startTime, velocity),
});
}
arc.setArcs(_arcs);
}
React.useEffect(() => {
makeArcs(10);
}, []);
React.useEffect(() => {
console.log(arcs);
}, [arcs]);
return (<canvas ref={ref} className="polycanvas" />);
// Ref is used for drawing to the canvas - not relevant in this example
}
Console only prints []
, nothing actually renders unless I write to a file and Vite does its HMR magic.
Is there anything I'm doing wrong? Is this behavior not supported by Redux?
EDIT: Added CodeSandbox demo.
The reducers in your sandbox are still incorrectly reassigning the state value and/or mutating and returning the state value.
See Direct State Mutation for further details.
export const polycanvas = createSlice({
name: 'polycanvas',
initialState,
reducers: {
// Arcs
addArc: (state, action: PayloadAction<Arc>) => {
if (state.arcs.length < state.settings.arc.maxArcs) {
// ERROR: don't re-assign the state value
// state = {
// ...state,
// arcs: [...state.arcs, action.payload]
// };
// OK: mutate state directly
state.arcs.push(action.payload);
} else {
throw(`Number of arcs exceeds the maximum. (max: ${state.settings.arc.maxArcs}, got: ${state.arcs.length})`);
}
},
removeArc: (state, action: PayloadAction<number>) => {
if (state.arcs[action.payload]) {
state.arcs = state.arcs.filter((_, index) => index != action.payload);
} else {
throw(`Tried removing arc at index ${action.payload}. No such arc exists.`);
}
},
setArcs: (state, action: PayloadAction<Arc[]>) => {
if (action.payload.length < state.settings.arc.maxArcs) {
// ERROR: don't re-assign the state value
// state = {
// ...state,
// arcs: action.payload
// }
// OK: mutate state directly
state.arcs = action.payload;
} else {
throw(`Number of arcs exceeds the maximum. (max: ${state.settings.arc.maxArcs}, got: ${action.payload.length})`);
}
},
// Settings
settings_setLoops: (state, action: PayloadAction<number>) => {
state.settings.speed.loops = action.payload;
// ERROR: don't mutate & return state
// return state;
// Also OK, return entirely new state value
// return {
// ...state,
// settings: {
// ...state.settings,
// speed: {
// ...state.settings.speed,
// loops: action.payload,
// },
// },
// };
},
settings_setTimeframeSeconds: (state, action: PayloadAction<number>) => {
state.settings.speed.timeframeSeconds = action.payload;
// ERROR: don't re-assign the state value
// return state;
},
settings_setMaxArcs: (state, action: PayloadAction<number>) => {
state.settings.arc.maxArcs = action.payload;
// ERROR: don't re-assign the state value
// return state;
},
settings_setArcDistance: (state, action: PayloadAction<number>) => {
state.settings.arc.distance = action.payload;
// ERROR: don't re-assign the state value
// return state;
}
}
});
The additional issue is that the PolyCanvas
is editing/setting properties on a canvas
and canvas context ctx
variable, but these variables are re-declared each render cycle.
I suggest the following refactor:
useAnimationFrame: Update signature to consume a dependency array.
import React from "react";
export default (callback: (...args: any[]) => any, deps: any[] = []) => {
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change
const requestRef = React.useRef<number>();
const previousTimeRef = React.useRef<number>();
const animate = (time: number) => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime)
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current!);
}, deps);
}
Poluycanvas: Move the ctx
and canvas
variables outside the component (or use React refs) so they are stable references, and pass the selected arcs
state as a dependency for the useAnimationFrame
hook call.
import React from "react";
import { Arc, usePolycanvas } from "../redux/polycanvas";
import useAnimationFrame from "../hooks/useAnimationFrame";
type Coords2D = {
x: number;
y: number;
}
type Canvas = {
center: Coords2D;
start: Coords2D;
end: Coords2D;
length: number;
strokeStyle: CanvasRenderingContext2D['strokeStyle'];
lineWidth: CanvasRenderingContext2D['lineWidth'];
}
let ctx: CanvasRenderingContext2D | null;
let canvas: Canvas;
export default () => {
const canvasRef = React.createRef<HTMLCanvasElement>();
const startTime = performance.now();
const { arc, arcs, settings } = usePolycanvas();
const calculateImpactTime = (currentImpactTime: number, velocity: number) => {
return currentImpactTime + (Math.PI / velocity) * 1000;
}
const makeArcs = (arcCount: number): void => {
const _arcs = [];
for (var i = 0; i < arcCount; i++) {
const velocity = (4 * Math.PI * (settings.speed.loops - i)) / settings.speed.timeframeSeconds;
_arcs.push({
color: { r: 255, g: 255, b: 255, a: 0.3 },
velocity,
nextImpactTime: calculateImpactTime(startTime, velocity),
});
}
arc.setArcs(_arcs);
}
React.useEffect(() => {
makeArcs(10);
if (canvasRef.current) {
ctx = canvasRef.current?.getContext("2d");
canvasRef.current.width = canvasRef.current.clientWidth;
canvasRef.current.height = canvasRef.current.clientHeight;
const center: Coords2D = {
x: canvasRef.current.width / 2,
y: canvasRef.current.height / 2
};
const start: Coords2D = {
x: 0,
y: canvasRef.current.height / 2
};
const end: Coords2D = {
x: canvasRef.current.width,
y: canvasRef.current.height / 2
};
const length = end.x;
canvas = {
center, start, end, length,
strokeStyle: `rgba(255, 255,
255, 0.3)`,
lineWidth: 1,
}
}
}, []);
React.useEffect(() => {
console.log(arcs);
}, [arcs]);
const drawArcs = () => {
if (canvas && ctx) {
const initialArcRadius = canvas.length * settings.arc.distance;
const spacing = (canvas.length / 2 - initialArcRadius) / arcs.length;
arcs.forEach((arc: Arc, index: number) => {
if (ctx) {
const radius = initialArcRadius + (index * spacing);
ctx.beginPath();
ctx.arc(canvas.center.x, canvas.center.y, radius, Math.PI * 2, 0);
ctx.strokeStyle = `rgba(${arc.color.r}, ${arc.color.g},
${arc.color.b}, ${arc.color.a})`;
ctx.lineWidth = ctx.lineWidth;
ctx.stroke();
}
});
}
}
const clearCanvas = () => {
if (ctx) {
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
}
};
useAnimationFrame(() => {
clearCanvas();
drawArcs();
}, [arcs]);
return (
<canvas ref={canvasRef} className="poly-canvas" />
);
}