I created a donut chart components (DonutChart) that takes an array of values representing percentages. The values are derived from data gathered from a firestore db. When running this:
Warning: React has detected a change in the order of Hooks called by DonutChart. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
-
Previous render Next render
------------------------------------------------------
1. useMemo useMemo
2. useEffect useMemo
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
along with:
TypeError: undefined is not an object (evaluating 'prevDeps.length')
I am assuming the SKIA library uses 'useMemo' for it's animation computations.
Here is some of the code and then I'll explain what I have tried:
DonutChart
const DonutChart = ({ dataValues }: DonutChartProps) => {
const sortedValues = dataValues.sort((n1, n2) => n1 - n2);
const total = sortedValues.reduce((sum, n) => sum + n, 0);
const createRollingTotal = () => {
var rollingTotal = 0;
return sortedValues.map((val, index) => {
rollingTotal = index !== 0 ? rollingTotal + sortedValues[index - 1] : rollingTotal;
return index !== 0 ? total - rollingTotal : total;
});
};
const progress = createRollingTotal();
const progressValues = progress.map(useValue);
React.useEffect(() => {
progress.map((val, index) => animateChart(val, progressValues[index]));
return () => {};
}, []);
const animateChart = (value: number, state: SkiaMutableValue) => {
state.current = 0;
runTiming(state, value, {
duration: 1250,
easing: Easing.inOut(Easing.cubic),
});
};
return (
<Canvas style={{ height: OUTER_RADIUS + 200, width: OUTER_RADIUS + 200 }} mode="continuous">
<DonutChartSkia progress={progressValues} />
</Canvas>
);
};
export default DonutChart;
DonutChartSkia
const fromCircle = (cx: number, cy: number, r: number) =>
rrect(rect(cx - r, cy - r, 2 * r, 2 * r), r, r);
interface ProgressBarProps {
progress: SkiaValue<number>[];
}
interface DonutChartProps {
dataValues: number[];
}
const BLUR = 1;
const SHADOW_WIDTH = 1;
const INNER_RADIUS = 35;
const OUTER_RADIUS = 55;
const PATH_RADIUS = (INNER_RADIUS + OUTER_RADIUS) / 2;
const DonutChartSkia = ({ progress }: ProgressBarProps) => {
const font = useFont(require('../../assets/fonts/SF-Pro-Text-Semibold.otf'), 18);
const text = useComputedValue(() => `${Math.round(progress[0]?.current * 100)}%`, [progress[0]]);
if (font === null) {
return null;
}
const textWidth = font.getTextWidth('00°C');
const path = Skia.Path.Make();
path.addCircle(OUTER_RADIUS, OUTER_RADIUS, PATH_RADIUS);
const c = vec(PATH_RADIUS + 12, PATH_RADIUS + 12);
const renderPath = (val: SkiaValue<number>, index: number) => {
const color = CHART_COLORS[index];
return (
<Group key={index}>
<RadialGradient
colors={[color.secondary, color.primary]}
c={vec(OUTER_RADIUS, OUTER_RADIUS)}
r={70}
/>
<Path
path={path}
style="stroke"
strokeWidth={OUTER_RADIUS - INNER_RADIUS}
end={val}
strokeCap="round"
antiAlias={true}
/>
</Group>
);
};
return (
<Group transform={translate({ x: 70, y: 70 })}>
<Group>
<LinearGradient
start={vec(22, 22)}
end={vec(200, 200)}
colors={[primaryColor, secondaryColor]}
/>
<Box box={fromCircle(OUTER_RADIUS, OUTER_RADIUS, OUTER_RADIUS)}>
<BoxShadow dx={SHADOW_WIDTH} dy={SHADOW_WIDTH} blur={BLUR} color={shadowColor} />
<BoxShadow dx={-SHADOW_WIDTH} dy={-SHADOW_WIDTH} blur={BLUR} color={glareColor} />
</Box>
</Group>
<Group>
<RadialGradient colors={[shadowColor, primaryColor]} c={vec(128, 128)} r={128} />
<Box box={fromCircle(OUTER_RADIUS, OUTER_RADIUS, INNER_RADIUS)}>
<BoxShadow dx={SHADOW_WIDTH} dy={SHADOW_WIDTH} blur={BLUR} color={shadowColor} />
<BoxShadow dx={-SHADOW_WIDTH} dy={-SHADOW_WIDTH} blur={BLUR} color={glareColor} />
</Box>
</Group>
<Text
x={c.x - textWidth / 2}
y={c.y + font.getSize() / Math.PI}
font={font}
text={text}
color={textColor}
/>
<Group>{progress.map((val, index) => renderPath(val, index))}</Group>
</Group>
);
};
- I have tried removing the 'useEffect' and instead use 'useMemo'.
- Changed how I handle the API call, but still need to use 'useEffect' for that.
- Added (and removed) umount functions to the 'useEffect' hook.
I expect the DonutChart to rerender with each data change since it uses values from a redux store that is updated from firestore. I keep running into this error, and I can only assume it is caused by the SKIA library using 'useMemo'. Still, I am not sure and only having less than a year of self-taught React-native dev experience I am not sure if it is just me or some issue I should log on GitHub. I know this is all over the place but I am hoping someone might notice something I might be doing wrong and I might also learn from that.