I am developing react-native project.
I have created a custom component inside which I use a 3rd party library component. The 3rd party library component needs the useRef hook from my code so that I can control the library component.
import React, {useRef} from 'react';
...
const MyComponent = () => {
const ref = useRef();
return (
<View>
<Button
title="OPEN 3rd party component"
onPress={() => ref.current.open()}
/>
<LibraryComponent
ref={ref}
...
/>
</View>)
}
Now, I have a screen named MyScreen, I need to show MyComponent on MyScreen, and control that library component from MyScreen instead of MyComponent. So, I refactored MyComponent to this (I removed the Button and change ref to be a pass-in property):
const MyComponent = ({ref}) => {
return (
<View>
<LibraryComponent
ref={ref}
...
/>
</View>)
}
Then, in MyScreen :
import React, {useRef} from 'react';
import MyComponent from '../components/MyComponent';
export const MyScreen = () => {
const screenRef = useRef();
...
return (
<View style={styles.container}>
<Button onPress=>{()=>screenRef.current.open()}/>
<MyComponent ref={screenRef}>
...
</View>
)
}
But I get warning message: Function components cannot be give refs. Attempts to access this ref will fail. Did you mean to use React.ForwardRef() ?
My questions is:
How to get rid of that warning & having MyScreen control the library component with reference hook?
======== UPDATE =======
I changed the name of the property in MyComponent from ref to innerRef like #Rishabh Anand suggested. The warning disappeared.
const MyComponent = ({innerRef}) => {
...
}
MyScreen is now:
export const MyScreen = () => {
const screenRef = useRef();
...
return (
<View style={styles.container}>
<Button onPress=>{()=>screenRef.current.open()}/>
<MyComponent innerRef={screenRef}>
...
</View>
)
}
But now a new issue comes, when I click on the Button of MyScreen, app crashed with error undefined is not an object (evaluating screenRef.current.open). It seems the screenRef.current holds a null instead of the library component. Why?
You have to use React.forwardRef
From the docs: https://fr.reactjs.org/docs/forwarding-refs.html
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
const ref = React.createRef();
<FancyButton ref={ref}>Cliquez ici</FancyButton>;
You can after you set a ref, call some function inside your child component like this:
const FancyButton = React.forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
click: () => {
onClick();
},
}));
const onClick = () => {
console.log('Clicked.');
};
return (
<button onClick={onClick} className="FancyButton">
{props.children}
</button>
);
});
const ref = React.createRef();
<FancyButton ref={ref}>Cliquez ici</FancyButton>;
And call from your parent component
ref.current.click()
Related
I am trying to useContext provider in my midiMonitor function. The midi monitor function is a helper function. The issue is that the context results as undefined because it's not inside the profile context. I am trying to figure out how to give the midiMonitor access to the profileContext. I know it can be done if I import the helper function inside the home component however I don't want to import it in the home component because it has nothing the do with the home component.
Is there another way I can use the midiMonitor helper function and have access to the contents of the profileContext
const App = () => {
midiMonitor()
return(
<ProfileProvider>
<Home />
</ProfileProvider>
)
}
const Home = () => {
// some functions that have access to the Profile Provider
const {profileName} = useContext(ProfileContext)
return(
<View>
<Text>{profileName}</Text>
</View>
)
}
const midiMonitor = () => {
const {profileName} = useContext(ProfileContext)
if (profileName === 'default'){
// results are undefined. I know why but do not want to
//import in into the Home component as it has nothing to do with the home component
console.log('you are using default midi profile')
}
}
It would have been easier if you could "ProfileContext" and "ProfileProvider". Here is an example of the same. Hope this helps.
import { useState, createContext, useContext } from "react";
import ReactDOM from "react-dom/client";
const ProfileContext = createContext();
function Component1() {
const [user, setUser] = useState("Jesse Hall");
return (
<ProfileContext.Provider value={user}>
<h1>{`Hello ${user}!`}</h1>
<Component2 user={user} />
</ProfileContext.Provider>
);
}
function Component2() {
return (
<>
<h1>Component 2</h1>
<Component3 />
</>
);
}
function Component3() {
return (
<>
<h1>Component 3</h1>
<Component4 />
</>
);
}
function Component4() {
return (
<>
<h1>Component 4</h1>
<Component5 />
</>
);
}
function Component5() {
const user = useContext(ProfileContext);
return (
<>
<h1>Component 5</h1>
<h2>{`Hello ${user} again!`}</h2>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Component1 />);
Greeting everyone,
I'm writing a React Native application and have stumbled into something interesting.
This is the simplified version of my code:
//import statements
...
const Profile = (props) => {
return(
<Text>Hey {props.name}!</Text>
<TouchableOpacity onPress={() => navigation.goBack();}>Go back</TouchableOpacity>
);}
export function ScreenOne({ navigation }) {
<Profile name="HUFF">
}
export function ScreenTwo({ navigation }) {
//blah blah blah
}
export default function App(){
//rest of the code
...
}
So basically, I'm trying to use react-navigation to navigate between screens of my App. But the button inside of my Profile component doesn't work.
Consider that I navigate from ScreenTwo to ScreenOne
Thanks in Advance.
React navigation has useNavigation hook
import { useNavigation } from '#react-navigation/native'
// if you are not using react-navigation 5 or 6
// then there is package https://github.com/react-navigation/hooks
const Profile = (props) => {
const navigation = useNavigation();
return(
<Text>Hey {props.name}!</Text>
<TouchableOpacity onPress={() => navigation.goBack()}>Go back</TouchableOpacity>
);
}
For this, you might have to specify navigation via props on the Profile JSX tag, so it can access the parent's navigation inside the component. E.g.:
//import statements
...
const Profile = (props) => {
return(
<Text>Hey {props.name}!</Text>
<TouchableOpacity onPress={() => props.navigation.goBack()}>Go back</TouchableOpacity>
);
}
function ScreenOne({ navigation }) {
<Profile name="HUFF" navigation={navigation}>
}
function ScreenTwo({ navigation }) {
//blah blah blah
}
export default function App(){
//rest of the code
...
}
I am new to React Native Testing Library. For my React native app I am using styled components and Typescript. I fetched the data and pass to my flatList. In Flat-list's render item I have created one global component where it display all the data which is wrap with one touchable container. When user will click the that touchable opacity it will go to single product details screen.
For testing the component I have created one mock container. And I wrap my touchable opacity component. I followed this article to create mocked navigator. I want to test the touchable opacity component and it navigate to the next screen. But I am getting error and it says:
The action 'NAVIGATE' with payload
{"name":"ProductDetails","params":{"product":{"__typename":"Product","id":"6414893391048","ean":"6414893391048","name":"T
shirt","brandName":"Denim","price":13.79 }} was not handled by any
navigator.
This my component
const navigation = useNavigation();
const onPress = () => {
trackProductView({
id: item.id ,
name: item.name,
});
navigation.navigate(Routes.ProductDetails, { product: item });
};
return (
<TouchableOpacity
accessible={true}
{...a11y({
role: 'menuitem',
label: item.name,
})}
onPress={onPress} // This is my onPress function
>
<ItemContainer>
<ProductTitleText ellipsizeMode={'tail'} numberOfLines={2}>
{item.name}
</ProductTitleText>
<QuantityModifierWrapper>
<QuantityModifier item={item!} />
</QuantityModifierWrapper>
</ItemContainer>
</TouchableOpacity>
);
This is my mocked container
import React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import 'react-native-gesture-handler';
import { MockedProvider } from '#apollo/client/testing';
type Props = {
screen: any;
params?: any;
};
const Stack = createStackNavigator();
const MockedNavigator = ({ screen, params = {} }: Props) => {
return (
<MockedProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name='MockedScreen'
component={screen}
initialParams={params}
/>
</Stack.Navigator>
</NavigationContainer>
</MockedProvider>
);
};
export default MockedNavigator;
This is my mocked screen
import React from 'react';
import { View } from 'react-native';
type Props = {
children: React.ReactNode;
};
const MockedScreen = ({ children }: Props) => {
return <View>{children}</View>;
};
export default MockedScreen;
This is my test suite where I am getting failed test
import React from 'react';
import { fireEvent, render, cleanup } from 'skm/utils/testing_utils';
import Touchablecomponent from './Touchable';
import MockedNavigator from './MockNav';
import MockedScreen from './Mockscreen';
describe('<Touchablecomponent/> ', () => {
test("render with invalid data", async () => {
const screenName = 'ProductDetails';
const component = (
<MockedNavigator
screen={() => (
<MockedScreen>
<ProductItemSmall item={mockData} />
</MockedScreen>
)}
// params={{data: mockData }}
/>
);
const { getByA11yRole, debug, toJSON } = render(component);
const item = getByA11yRole('menuitem');
console.log(fireEvent.press(item));
});
})
I am developing a React-Native project with functional component.
Here is a very simple screen which renders a calculated result list. Since I need to calculation to be called only once so I put it inside the useEffect hook.
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const result = doCalculation();
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{result.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
As you can see I have called the doCalculation() to get the result inside useEffect hook. My question is how can I render the result in the return part? Since the result is calculated inside the hook, it is not accessible in the renderer.
P.S. Moving the const result = doCalculation() outside the useEffect hook is not an option since I need the calculation to be called only once.
Below is an example. According to the above comments it looks like you want it to be called once on component mount. All you really need to do is add a useState
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
const [calculatedData, setCalculatedData] = useState([])
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const result = doCalculation();
setCalculatedData(result)
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{calculatedData.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
const [calculatedData, setCalculatedData] = useState([])
useState is a hook used to store variable state. When calling setCalculatedData inside the useEffect with empty dependency array it will act similar to a componentDidMount() and run only on first mount. If you add variables to the dependency array it will re-run every-time one of those dep. change.
You can change the data inside the calculatedData at anytime by calling setCalculatedData with input data to change to.
Make use of useState to save the calculation result and then use the variable inside return. See https://reactjs.org/docs/hooks-state.html.
Code snippet:
import {doCalculation} from '../util/helper'
const MyScreen = ({navigation}) => {
const [result, setResult] = useState([]);
useEffect(() => {
// I call a function from a helper module here.
// The result is a list of object.
const tempRes = doCalculation();
setResult(tempRes);
// eslint-disable-next-line
}, []);
// renderer
return (
<View>
// Problem is 'result' is not accessible here, but I need to render it here
{result.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
Is async function?
if the function is not async (not wating for respond like from api) - you don't need useEffect.
import React from 'react';
import { Text, View } from 'react-native';
import {doCalculation} from '../util/helper'
const results = doCalculation();
const MyScreen = () => {
return (
<View>
{results.map(item=> <Text key={item.id}> {item.value} </Text>)}
</View>
)
}
export default MyScreen;
else you should wait until the results come from the server..
import React, { useState, useEffect } from 'react';
import { Text, View } from 'react-native';
import { doCalculation } from '../util/helper';
const MyScreen = () => {
const [results, setResults] = useState(null) // or empty array
useEffect(() => {
(async () => {
setResults(await doCalculation());
})();
}, []);
return (
<View>
{results?.map(item => <Text key={item.id}> {item.value} </Text>) || "Loading..."}
</View>
)
}
export default MyScreen;
and I can use more readable code:
if (!results) {
return <View>Loading...</View>
}
return (
<View>
{results.map(item => <Text key={item.id}> {item.value} </Text>)}
</View>
)
the async function can be like:
const doCalculation = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 1, value: 1 }]);
}, 2000);
});
};
Perhaps what I think can solve my issue is not the right one. Happy to hearing ideas. I am getting:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and async task in a useEffect cleanup function
and tracked it down to one component that is in my headerRight portion of the status bar. I was under the impression it mounts only once. Regardless, the component talks to a syncing process that happens and updates the state. For each status of the sycing, a different icon is displayed.
dataOperations is a NativeModules class that talks to some JAVA that does the background syncing and sends the status to RN.
import React, {useState, useEffect} from 'react';
import {DeviceEventEmitter } from 'react-native';
import DataOperations from "../../../../lib/databaseOperations"
const CommStatus: () => React$Node = () => {
let [status, updateStatus] = useState('');
const db = new DataOperations();
const onCommStatus = (event) => {
status = event['status'];
updateStatus(status);
};
const startSyncing = () => {
db.startSyncing();
};
const listner = DeviceEventEmitter.addListener(
'syncStatusChanged',
onCommStatus,
);
//NOT SURE THIS AS AN EFFECT
const removeListner = () =>{
DeviceEventEmitter.removeListener(listner)
}
//REMOVING THIS useEffect hides the error
useEffect(() => {
startSyncing();
return ()=>removeListner(); // just added this to try
}, []);
//TODO: find icons for stopped and idle. And perhaps animate BUSY?
const renderIcon = (status) => {
//STOPPED and IDLE are same here.
if (status == 'BUSY') {
return (
<Icon
name="trending-down"
/>
);
} else if (status == 'IS_CONNECTING') {
...another icon
}
};
renderIcon();
return <>{renderIcon(status)}</>;
};
export default CommStatus;
The component is loaded as part of the stack navigation as follows:
headerRight: () => (
<>
<CommStatus/>
</>
),
you can use App.js for that.
<Provider store={store}>
<ParentView>
<View style={{ flex: 1 }}>
<AppNavigator />
<AppToast />
</View>
</ParentView>
</Provider>
so in this case will mount only once.