I've created a simple canvas drawing app using React Native and Skia, which works for the most part, but isn't saving drawing strokes correctly. It's based loosely off of these two articles, with the thought being that I could simplify the implementation since a.) Skia already has a touchHandler facility so I don't need Gesture Handler, and b.) I should be able to leverage the useState hook instead of a state handler like zustand.
The problem I'm running into is that the paths state variable is getting updated appropriately (as demonstrated by the useEffect console log output) on the first touch event, but appears to be empty on each subsequent touch event. The net effect is that I can draw a series of paths but only the last one is preserved and displayed on the canvas.
My dev environment is Windows using react-native-cli instead of expo. Node is installed via nvm-windows. Project was bootstrapped using react-native init <project-name> and launched via npx react-native run-android I modified App.tsx to import StateTest from a file containing the code below. I can reproduce the behavior on a physical Pixel 5 and on an Android Studio virtual device. Relevant versions are:
NPM 9.2.0
#shopify/react-native-skia 0.1.172
react-native 0.71.1
Here's the code; I'm using:
import {
Canvas,
Path,
Skia,
Group,
useTouchHandler,
} from "#shopify/react-native-skia";
import { useState, useRef, useCallback, useEffect } from 'react';
export const StateTest = () => {
let workingPath = [];
const [ displayPaths, setDisplayPaths ] = useState([])
const [ paths, setPaths ] = useState([]);
const onDrawingStart = useCallback(({ x, y }) => {
workingPath = [`M ${x} ${y}`];
});
const onDrawingActive = useCallback(({ x, y }) => {
if (!workingPath){
return;
}
workingPath.push(`L ${x} ${y}`);
});
const onDrawingFinished = useCallback(() => {
if (!workingPath){
return;
}
savePaths();
workingPath = [];
});
const touchHandler = useTouchHandler({
onActive: onDrawingActive,
onStart: onDrawingStart,
onEnd: onDrawingFinished,
});
/* I thought maybe the problem was with the touchHandler operating on
an old copy of state, so I pulled that code out here, which didn't
fix the problem; that's not surprising, since the callback would
necessarily be operating on the same version of state as the
touchHandler
*/
const savePaths = useCallback(() => {
let localPath = [...paths];
console.log('before:');
console.log(localPath);
localPath.push(workingPath.join(" "));
console.log('after:');
console.log(localPath);
setPaths([...localPath]);
});
const drawPaths = useCallback(() => {
console.log("setting displaypaths from:");
console.log(paths);
setDisplayPaths(
<Group>
{paths.map((p) =>
<Path key={p} path={p} color="red" style="stroke" strokeWidth={4} />
)}
</Group>
)
});
useEffect(() => {
console.log("paths have been updated:");
console.log(paths);
drawPaths();
}, [paths])
useEffect(() => {
console.log("paths have been drawn:");
console.log(displayPaths);
}, [displayPaths])
return (
<Canvas style={{ flex: 1 }} onTouch={touchHandler}>
{displayPaths}
{ /* This may be related; I couldn't get anything to display using the array map
here, so I created the drawPaths() function and inserted {displayPaths}
above, which seems to work except for the problem with outdated state.
paths.map(p => {
//console.log(`mapped |${p.toSVGString()}|`);
<Path key={p.toSVGString()} path={p} color="red" style="stroke" strokeWidth={10} />
}
) */ }
</Canvas>
);
};
The debug output looks like this. I started the app and made two finger swipes on the canvas.
LOG before:
LOG []
LOG paths have been updated:
LOG ["M 283.6363636363636 233.45454545454547 L 283.6363636363636 233.45454545454547 L 283.6363636363636 233.45454545454547 <elided for space>"]
LOG setting displaypaths from:
LOG ["M 283.6363636363636 233.45454545454547 L 283.6363636363636 233.45454545454547 L 283.6363636363636 233.45454545454547 <elided for space>"]
LOG paths have been drawn:
LOG <Group><Path color="red" path="M 283.6363636363636 233.45454545454547 L 283.6363636363636 233.45454545454547 <elided for space>" strokeWidth={4} style="stroke" /></Group>
LOG before:
LOG []
LOG paths have been updated:
LOG ["M 141.45454545454547 240.72727272727272 L 141.45454545454547 240.72727272727272 L 141.45454545454547 240.72727272727272 <elided for space>"]
LOG setting displaypaths from:
LOG ["M 141.45454545454547 240.72727272727272 L 141.45454545454547 240.72727272727272 L 141.45454545454547 240.72727272727272 <elided for space>"]
LOG paths have been drawn:
LOG <Group><Path color="red" path="M 141.45454545454547 240.72727272727272 L 141.45454545454547 240.72727272727272 <elided for space>" strokeWidth={4} style="stroke" /></Group>
As you can see, the first input updated the paths or the useEffect wouldn't have output the 'paths have been updated' message, but on the second input, the paths array is empty when the touchHandler's onEnd event calls my onDrawingFinished callback. I would expect that by the time I draw the second path, it would observe the updated state, but I'm not confident in my understanding of react internals, so it's entirely possible I've just overlooked some facet of useState or Skia.
Related
I am writing test cases for ImageBackground and Image component in React native but the test cases is getting failed as I am using local assets. It is unable to fetch the location of the assets folder I guess. Can someone help me out with this issue?
test('renders Background image', () => {
const { getByTestId } = render(LandingScreen);
expect(getByTestId('logo').toBeInTheDocument);
})
test('Logo must have src = "/logo.svg" and alt = "Logo"', () => {
render(LandingPage);
const logo = screen.getByRole('logo');
expect(logo).toHaveAttribute('source', '../../../assets/logo.png');
});
it(" Product introduction check background Image present or not", () => {
const logoImg = LandingScreen.root.findByProps({
testID: "logo",
}).props;
expect(logoImg).toBeDefined();
});
it('should display a local image when given a valid image file path', () => {
// Render the Image component with the provided image file
const imageComponent = shallow(<Image source={require("../../assets/authBg.png")} />);
expect(imageComponent.find('logo').prop('src')).toEqual(logo);
})
I have tried these code for test cases but getting error this kind of error below are attached Screenshots
[enter image description here](https://i.stack.imgur.com/UX1mg.png)
I was developing a useUser Hook for per-page authentication. I have implemented the useUser hook normally and Redirecting works fine accordingly.
But I am getting the above error.
Abort fetching component for route: "/login"
How can I fix useUserHook to solve it??
//useUser.tsx
const useUser = ({ redirectTo, redirectIfFound }: IParams) => {
const { data, error } = useRequest("authed", isAuthed);
const user = data?.data;
const hasUser = user;
useEffect(() => {
if (!redirectTo) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !hasUser) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && hasUser)
) {
Router.push(redirectTo);
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
//index.tsx
const Home: NextPage = () => {
const user = useUser({ redirectTo: "/login" });
if (user === undefined || user === false) {
return <div>Loading...</div>;
}
return (
<div>
<Head>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div>Home</div>
</div>
);
};
UseRequest Hook returns true and false as return values.
tl;dr
Ensure that you only call router.push() once throughout all potential re-executions of useEffect with the help of state:
const [calledPush, setCalledPush] = useState(false); // <- add this state
// rest of your code [...]
useEffect(() => {
if (!redirectTo) return;
if (
(redirectTo && !redirectIfFound && !hasUser) ||
(redirectIfFound && hasUser)
) {
// check if we have previously called router.push() before redirecting
if (calledPush) {
return; // no need to call router.push() again
}
Router.push(redirectTo);
setCalledPush(true); // <-- toggle 'true' after first redirect
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
Background
useEffect potentially gets called multiple times if you have more than one dependency (Also happens with React Strict Mode enabled, but in this case there seems to be no error), and (re-)calling router.push() multiple times within the same Next.js page in different places/throughout different re-renders seems to cause this error in some cases, as the redundant router.push() call(s) will have to be aborted, because the current page-component unmounts due to the successful, previously called router.push().
If we keep track of whether we have already called router.push via the calledPush state as in the code snippet above, we omit all redundant router.push() calls in potential useEffect re-executions, because for all subsequent useEffect executions the state value calledPush will already be updated to true as useEffect gets triggered after re-renders, hence after setCalledPush(true) takes effect.
In my case I have use rotuer.push("/") two times in a single file. That caused the error. Try using one. I think problem will be solved.
This error occurs because useEffect tries to update a component that has already been unmounted and this can introduce memory leaks in which your app uses more memory than it needs to. To prevent this, use the following approach:
useEffect(() => {
//first manually mount the effect
let mounted = true;
//check if component is currently mounted
if(mounted && ...code){
router.push('/index')
}
//cleanup side effects before unmounting
return()=>{mounted=false}
}, [router]);
};
In my current NextJS project, when I make reactStrictMode: false,, the value to false, then it seems like the re-rendering will be gone, and component will be only rendered once.
I don't like react strict mode very much ..
I'm writing a simple app with React Native and Expo.
This app has ~10 small to medium sized images that are used in different places within the app.
From what I read, unless I cache these images, they will be required to be downloaded from expo each time.
For this reason, I have noticed that they seem to load in really slowly when testing the app. Upon building and navigating through the app, I find that it takes a few seconds for my images to pop up even after the rest of the page has loaded.
I followed the setup as seen in the starting template.
Here is what my App.js looks like (I am using react-navigation so it varies from the sample file above):
export default class App extends React.Component {
state = {
isLoadingComplete: false
};
componentDidMount() {
StatusBar.setHidden(true);
}
render() {
_loadResourcesAsync = async () => {
return Promise.all([
Asset.loadAsync([
require("./assets/syria.png"),
require("./assets/lebanon.png"),
require("./assets/kenya.png"),
require("./assets/indonesia.png"),
require("./assets/somalia.png"),
require("./assets/india.png"),
require("./assets/america.png"),
require("./assets/albania.png"),
require("./assets/bosnia.png")
])
]);
};
_handleLoadingError = error => {
Alert.alert(error);
};
_handleFinishLoading = () => {
this.setState({ isLoadingComplete: true });
};
if (this.state.isLoadingComplete == false) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
);
} else {
return (
<AppContainer
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
);
}
}
}
I have excluded my react-navigation code for the sake of brevity.
When I run this, my app gets stuck at Downloading JavaScript bundle 100.00%.
It seems that the _handleFinishLoading never runs. At least, that's the only reason I can see for it to never finish loading.
Given the small amount of images, I don't know how this could take more than a second. Instead it sits at the splash screen forever.
Any ideas on what I might be doing wrong here?
Found the solution:
I made a simple error. The async functions (_loadResourcesAsync, _handleFinishLoading, etc) need to be outside the render method. Moving them below my render method inside of the app class caused this to work as expected.
Developing an admin that has no need for translations. Is there a way to turn them off completely. As it is now, for things like doing notifications, they display but I also get a console warning about missing key for translation.
In addition to Frederik's answer, here's the right way to disable the 'missing translations' warnings on react-admin:
import polyglotI18nProvider from 'ra-i18n-polyglot'; // install the package
import englishMessages from 'ra-language-english'; // install the package
const App = () => {
const i18nProvider = polyglotI18nProvider(() => englishMessages, 'en', { allowMissing: true });
return (
<Admin i18nProvider={i18nProvider}
...
/>
)
}
Solved it by adding a custom i18nProvider that allows missing keys:
const i18nProvider = polyglotI18nProvider(locale => i18nMessages[locale], 'en', { allowMissing: true });
<Admin
i18nProvider={i18nProvider}
...
/>
More details: https://marmelab.com/react-admin/Translation.html
and: https://www.npmjs.com/package/node-polyglot#options-overview
I'm trying to write some tests for a React app I've been working on, and I figured I would use Jest since it's mentioned in the React docs.
I'm using Webpack and so far I've installed jest-cli, babel-jest, and I included the following configuration in package.json:
"jest": {
"scriptPreprocessor": "./node_modules/babel-jest",
"unmockedModulePathPatterns": [
"./node_modules/react",
"./node_modules/react-dom"
],
}
So, I'm writing the tests for some file foo.js. This file includes some other module bar.js (i.e. const bar = require('./bar');). Unfortunately, when I run jest I get the following error:
SyntaxError: Block-scoped declarations (let, const, function, class) not yet
supported outside strict mode in file 'js/foo.js'.
So, after some research, I find out I have to include 'use strict'; at the top of foo-test.js. However, for some reason, I still get the same error unless I also include 'use strict'; at the top of foo.js.
So my question is: am I doing something wrong? If not, is there anyway for me to write my tests using Jest without having to write 'use strict'; at the top of all my source files?
It seems to test out basic ES2015 classes with jest, use strict is required, however to test React Components, 'use strict' isn't required. Here's an example
//CheckboxWithLabel.js
import React, {Component} from 'react';
class CheckboxWithLabel extends Component {
constructor(){
super(...arguments);
this.state = {
isChecked: false
};
this.onChange = this.onChange.bind(this);
}
onChange() {
this.setState({
isChecked: !this.state.isChecked
});
}
render() {
return (
<label>
<input type="checkbox"
checked={this.state.isChecked}
onChange={this.onChange} />
{this.state.isChecked ? this.props.labelOn : this.props.labelOff }
</label>
);
}
}
export default CheckboxWithLabel;
//CheckboxWithLabel_tests.js
jest.disableAutomock(); //use this instead of jest.autoMockOff()
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import CheckboxWithLabel from '../source/components/CheckboxWithlabel';
// const CheckboxWithLabel = require('../source/components/CheckboxWithLabel');
describe('CheckboxWithlabel', () => {
const shallowRenderer = TestUtils.createRenderer();
//render a checkbox
const checkbox = TestUtils.renderIntoDocument(
<CheckboxWithLabel labelOn="On" labelOff="Off" />
);
// shallowRenderer.render(<CheckboxWithLabel labelOn="On" labelOff="Off" />)
// const checkbox = shallowRenderer.getRenderOutput();
// it('defaults to unchecked and off label', () => {
// const inputField = checkbox.props.children[0];
// const textNode = checkbox.props.children[1];
// expect(inputField.props.checked).toBe(false);
// expect(textNode).toEqual('Off');
// })
var checkboxNode = ReactDOM.findDOMNode(checkbox);
// let checkboxElement = TestUtils.findRenderedDOMComponentWithTag(checkbox, 'input');
it('defaults to Off label', () => {
expect(checkboxNode.textContent).toEqual('Off');
// expect(checkboxElement.checked).toBe(false);
});
})
Edited: This is not required anymore
Notice the only caveat being that you have to explicitly add a auto_mock_off.js file that simply adds this line (it took me hours to figure this one out)
jest.autoMockOff();
More information can be found on this thread on github Github React Issue #932
That's it! the component testing works perfectly. I've also tried the same example with shallow rendering and it worked perfectly too! Hope this helps!