I want to pass a prop to the screen. When I try that inline e.g (props) => <Comp {...props} {...customProps} /> I get a warning message, that I shouldn't parse functions to that component property. Okay. I thought I'll just create functions for every component which needs custom props. It is working, but is there a better solution? Here is my component:
export default function Loading() {
const [loggedIn, setLoggedIn] = React.useState(false);
const Stack = createStackNavigator();
const authService: AuthService = new AuthService();
const authProps: IAuthProps = {
authService
};
/**
* Bind neccessary props to the login component
* #param props Props
*/
function LoginWithProps(props) {
return <Login {...props} {...authProps} />;
}
/**
* Bin neccessary props to the registration component
* #param props Props
*/
function RegistrationWithProps(props) {
return <Registration {...props} {...authProps} />;
}
return (
<>
{/*Show the app, when logged in*/}
{loggedIn === true ? (
<View>
<Text>Test</Text>
</View>
) : (
<Stack.Navigator
initialRouteName="Login"
screenOptions={{ headerShown: false, animationEnabled: false }}
>
<Stack.Screen name="Login" component={LoginWithProps} />
<Stack.Screen name="Registration" component={RegistrationWithProps} />
</Stack.Navigator>
)}
</>
);
}
```
Yours is not a good solution to the problem because new types of LoginWithProps and RegistrationWithProps components will be created every render, meaning old ones will be unmounting and new ones mounting every time. The same thing that happens when you pass a function, but without a warning
You can't pass props to these screens, as it is not the Loading component that is a direct parent to these screens. If you want to pass data to customize those components, you need to do it through navigation params, in 2 ways:
when navigating to a screen navigation.navigate('Login', { param: 'hello' })
by proviging initial params
.
<Stack.Screen
name="Login"
component={Loaing}
initialParams={{ param: 'hello' }}
/>
And read it in Login with props.route.params
Note though this is called initialParams for a reason - they are not reactive, changing them after component is mounted has no effect (it is also possible to change them from inside component). If you want to pass reactive params, use React Context or Redux
Passing parameters to routes
Related
Imagine a simple form, which renders multiple custom components applying some layout tweaks.
The custom components are: <UsernameInput />, <PasswordInput />, <DateTimePicker />, <FancyButton />, <Checkbox />
And the JSX of the form component looks like:
return (
<View style={globalStyles.flex}>
<View style={styles.inputsContainer}>
<UsernameInput
... // a lot of props
/>
<PasswordInput
... // a lot of props
/>
<DateTimePicker
... // a lot of props
/>
</View>
<View style={styles.footer}>
<Checkbox />
<FancyButton type="submit" onPress={...} />
</View>
</View>
);
What if we try to split the component's render method into multiple inner sub-render functions?
const LoginForm = ({ ... }) => {
...
/**
* Renders the inputs of the form.
*
* #returns {React.ReactElement} The inputs.
*/
const renderInputs = () => (
<View style={styles.inputsContainer}>
<UsernameInput
... // a lot of props
/>
<PasswordInput
... // a lot of props
/>
<DateTimePicker
... // a lot of props
/>
</View>
);
/**
* Renders the submit button.
*
* #returns {React.ReactElement} The footer of the form.
*/
const renderFooter = () => (
<View style={styles.footer}>
<Checkbox />
<FancyButton type="submit" onPress={...} />
</View>
);
return (
{renderInputs()}
{renderFooter()}
);
};
Those helper methods returns JSX! They are not simple vanilla functions, they are components! WE ARE RECREATING COMPONENTS ON EACH RENDER!
In order to handle this, what I do is converting the inner functions to inner memoized components, like this:
/**
* Renders the submit button.
*
* #type {React.FC}
* #returns {React.ReactElement} The footer of the form.
*/
const Footer = useCallback(() => (
<View style={styles.footer}>
<Checkbox />
<FancyButton type="submit" onPress={...} />
</View>
), [deps]);
return (
<View style={globalStyles.flex}
<Inputs />
<Footer />
</View>
);
Is this considered an anti-pattern? I mean, should we avoid splitting the JSX when there is no reason to create a new component in the global scope of the app (in a new module or outside the component)?
Is this considered an anti-pattern?
There's no point in using useCallback on those functions if you don't provide those functions as props to other components that optimize their rendering by not re-rendering when their props don't change. (Lots of people misunderstand useCallback, it's a bit subtle.) You're still recreating them every time (you have to create the function to pass it into useCallback), you're just then doing even more work (processing deps) to decide whether to use the newly-created function or the previously-created function. It's overhead without purpose, and not what useCallback is for. The purpose of useCallback is to create stable or stable-as-possible function references for passing to child components that may be able to avoid re-rendering if you don't change the functions they receive.
Instead, either:
Make those components (or just helper functions, depending) in your module. (No need to export them if they're only used by your component.) That way, they're only created once (when the module is loaded).
If you think about it, this is like any other function that's gotten too big: You break it into parts, and put those parts into smaller functions you call. Same concept.
Or
Create the functions once, on the first render (ensuring of course that they don't close over anything they need; pass it to them instead), storing the result on an object on a ref via useRef:
const fnRef = useRef(null);
if (!fn.Ref.current) {
fnRef.current = {
Footer: /*...*/,
SomethingElse: /*...*/,
// ...
};
}
const { Footer, SomethingElse /*, ...*/ } = fnRef.current;
// ...
That way, they truly are created only once, on mount.
I would strongly lean toward #1.
I am trying to implement authentication flow with Redux in my app, I am using conditional rendering in my App.js file to show either app screen or authentication screen.
App.js:
<Provider store={store} >
<NavigationContainer>
{store.getState().auth.initializing && <Initializing />}
{store.getState().auth.isLoggedIn && (
<AppNavigator/>
)}
{!store.getState().auth.initializing &&
!store.getState().auth.isLoggedIn && (
<AuthenticationNavigator/>
)}
</NavigationContainer>
</Provider>
The problem is it doesn't react on changes in the state, when I press Login button on the login screen even though redux refreshes the state, the conditional rendering in App.js doesn't react anyhow and I still see login screen. If I use useState with isUserLoggedIn flag in the App.js, and pass callback to the components it works, but not with Redux.
Please help, what do I do wrong?
The Redux store.getState is only a function that returns the current state, it doesn't subscribe to changes to the store object. This only works when the component rendering your conditionals is rerendered.
What you can do is either use the subscribe method to subscribe to changes in the store, caching the state locally:
const [auth, setAuth] = React.useState(() => store.getState().auth);
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setAuth(store.getState().auth);
});
return unsubscribe;
}, []);
...
<NavigationContainer>
{auth.initializing && <Initializing />}
{auth.isLoggedIn && (
<AppNavigator/>
)}
{!.auth.initializing &&
!.auth.isLoggedIn && (
<AuthenticationNavigator/>
)}
</NavigationContainer>
Or you and use the useSelector from react-redux to effectively do the same thing:
const auth = useSelector(state => state.auth);
...
<NavigationContainer>
{auth.initializing && <Initializing />}
{auth.isLoggedIn && (
<AppNavigator/>
)}
{!.auth.initializing &&
!.auth.isLoggedIn && (
<AuthenticationNavigator/>
)}
</NavigationContainer>
Note that both of these solutions absolutely require the Redux store Provider to be moved higher in the ReactTree so that this component can consume the redux context and access the store's value.
Note: The component prop accepts component, not a render function. Don't pass an inline function (e.g. component={() => }), or your component will unmount and remount losing all state when the parent component re-renders. See Passing additional props for alternatives.
This warning from the simulator.
enter image description here
function HomeScreen(props: Object) {
return (
<Navigator initialRouteName="Empty1">
<Screen
name="Empty1"
component={() => {
return (
<View>
<Text>Example Text</Text>
</View>
);
}}
/>
<Screen name="Home1" component={HomeScreen1} />
</Navigator>
);
}
It's warning you that component is going to be re-rendered every time screen is re-rendered. This is because every time the component arg is evaluated it returns a fresh inline function. (the prop changes every time thus it must always re-render)
Evaluation of the same inline function twice yields different objects.
Eg.
let funcs = []
for (let i =0 ; i<2; i++) {
funcs.push(() => 1)
}
funcs[0] == funcs[1]
>>> false
You can wrap this is useCallback to get a stable function that only updates when it needs to (never in your case since it captures no state). More on hooks here
function HomeScreen(props: Object) {
let component = useCallback(() => {
return (
<View>
<Text>Example Text</Text>
</View>
);
}, [] // no dependencies. This lambda will only be evaluated once.
);
return (
<Navigator initialRouteName="Empty1">
<Screen
name="Empty1"
component={component}
/>
<Screen name="Home1" component={HomeScreen1} />
</Navigator>
);
}
Or since this function doesn't depend on anything stateful you could also just declare it globally at the top of this file.
let component = () => {
return (
<View>
<Text>Example Text</Text>
</View>
);
};
function HomeScreen(props: Object) {
return (
<Navigator initialRouteName="Empty1">
<Screen
name="Empty1"
component={component}
/>
<Screen name="Home1" component={HomeScreen1} />
</Navigator>
);
}
You need to understand what is hiding behind JSX syntax.
When you use <SomeComponent/> it is added to the React Virtual DOM and exists there until the tree is changing and component has to be unmounted. It survives rerenders, the DOM is updating, but component lives.
When you use () => <SomeComponent /> this component is a new object on every rerender of the parent. If it has state (via setState hook) it will be reinitialized. If it has some code in useEffect(() => {}, []) hook it will be called again.
Experts am in the learning stage and I wanted to know how can I can get the text input value from the search bar and pass it to my another component where I have an API URL as a search query.
I have seen many videos which use props and I guess that’s the correct way if not wrong to send the data between components. However, I tried to use that but I have an issue. I can not see the props value which I have passed from my nav component (textfiled value) to the product component.
any help how can I do that.
Please if you can provide a source code explanation would be great.
Also please find the relevant code below:
Please see the full code here https://codesandbox.io/s/determined-nightingale-m2mtn?file=/src/App.js
This is the code on my nav component and I want the onchange value to be passed to the product component.
const Navbar = () =>{
const[searchText, setSearchText] = useState();
const handleChange = (event) =>{
setSearchText(event.target.value)
}
return (
<>
<div className="nav_main">
<div className="logo_main">
<Link exact to='/home' className="logo" > Movie </Link>
</div>
<div className="search_main">
<TextField
id="input-with-icon-textfield"
className= "searchTextStyle"
size= "small"
placeholder="search"
variant="standard"
value = {searchText}
onChange = {handleChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{color:'white'}} />
</InputAdornment>
),
}}
/>
</div>
<div className="nav_menu">
<NavLink exact activeClassName= "active_link" to='/home' className="navitems" > Home </NavLink>
<NavLink exact activeClassName= "active_link" to='/about'className="navitems" > About </NavLink>
<NavLink exact activeClassName= "active_link" to = '/products' className="navitems" > Products </NavLink>
<IconButton className="account_icon">
<AccountCircleIcon fontSize="small"/>
</IconButton>
</div>
</div>
</>
)
};
export default Navbar;
The Product component:
const Produts = (props) =>{
console.log(props.value);
return (
<>
<h1>{`http://api.themoviedb.org/3/search/movie?api_key=Mykey&query=${props.value}`}</h1>
</>
)
};
export default Produts;
Thanks
how are you?
What happens is, it's okay, you are getting the data from de input, but you are not passing it forward to the other component where you want to use that data.
How can you do that?
As they are 'sibling' components, i recommend you to put the handleChange and the useState where you are going to save the data in the FATHER component, pass them through props for the component where the user types, and then pass the stored data also via props for the component where you are going to use it.
Let me give you a pratical example of how you can do it:
On the App you handle the state and provide it for the components via props.
function App() {
const [searchText, setSearchText] = useState();
const handleChange = (event) => {
setSearchText(event.target.value);
};
return (
<>
<Navbar handleChange={handleChange} searchText={searchText}/>
<Switch>
<Route path="/products"
render= { (props) => <Produts {...props} searchText={searchText} /> }
/>
</Switch>
</>
);
}
in the NavBar component you get them via props, destructuring it:
const Navbar = ({handleChange, searchText}) => {
return (
<>
<div className="nav_main">
<div className="logo_main">
<Link exact to="/home" className="logo">
{" "}
Movie{" "}
</Link>
</div>
and in the Produts component you get them also via props:
const Produts = (props) => {
console.log(props.searchText);
const classes = useStyles();
const [movieData, setMovieData] = useState([
// the below is an fake api data which needs to be deleted when final disgn is ready and add real api from useEffect
{
adult: false,
observe that, before you were getting "props.value" but the name that you get in the component is the same name that you used to pass it from the provider component, in this case 'searchText'.
tl;dr
You need to pass them via props, choosing a name, and you get them from the other side using the same name. To get them you can either use 'props.PROP_NAME' or you can use the destructuring method and get the name directly.
Also, to pass a props through a Router rendered component you have to use this syntax:
render= { (props) => <Produts {...props} PROP_NAME={VARIABLE_YOU_ARE_USING}
On the NavBar and the Produts components i used different methods so you can see that you can use both for getting the props information.
I hope that can help, anyway i'm available for further explanations.
The NavLink has additional properties which can be used. The 'to' attribute needn't be only a string value. For example:
<NavLink exact activeClassName= "active_link" to = '/products' className="navitems" > Products </NavLink>
can be rewritten as
to={{
pathName: '/products',
someProps:{data: 'Place any Data here'}
}}
className="navitems" > Products </NavLink>
Then in your products component you should be able to retrieve the data using this.props.location.someProps.data.
Take a look at these articles:
https://reactrouter.com/web/api/Link/to-object,
https://medium.com/#bopaiahmd.mca/how-to-pass-props-using-link-and-navlink-in-react-router-v4-75dc1d9507b4
They outline exactly how this should be used.
I have a Layout component which wraps the rest of my application. The default route is a page with multiple buttons, implemented as a small component called NavButton, which use history.push to go to a new route. Then in my Layout component, there are 2 buttons, but one of them should change depending on which route we are currently navigated to. So when we are on the main page, the button should say i.e. "Change Code", but on any other page, it should say "Back". I tried it in the following way:
Layout.tsx:
interface ILayoutProps {
children: ReactNode;
}
const Layout: React.FC<ILayoutProps> = ({ children }) => {
const currentRoute = useLocation();
useEffect(() => {
console.log(currentRoute);
}, [currentRoute]);
const logout = () => {
console.log('logout');
};
return (
<div>
<div>{children}</div>
<Grid container spacing={2}>
<Grid item xs={6}>
{currentRoute.pathname === '/' ? (
<NavButton displayName="Change Code" route="/change-code" variant="outlined" />
) : (
<Button variant="outlined" color="primary" fullWidth>
Back
</Button>
)}
</Grid>
...
</Grid>
</div>
);
};
export default Layout;
NavButton.tsx:
interface NavButtonProps {
displayName: string;
route: string;
variant?: 'text' | 'outlined' | 'contained';
}
const NavButton: React.FC<NavButtonProps> = ({ displayName, route, variant = 'contained' }) => {
const history = useHistory();
const navigatePage = () => {
history.push(route);
// Also tried it like this:
// setTimeout(() => history.push(route), 0);
};
return (
<Button
variant={variant}
color="primary"
onClick={() => navigatePage()}
fullWidth
>
{displayName}
</Button>
);
};
export default NavButton;
SO in my Layout, I am trying to keep track of location changes with the useLocation() hook, but when a button is pressed, and thus history.push(route) is called, it doesn't get detected by the hook in Layout. I've also tried to use the useHistory() hook and call the listen() method on it, but same problem. Also, my router is a BrowserRouter, not sure if that is of any help... What am I missing here?
So the issue was that I had my App component defined like this:
<Layout>
<Routes />
</Layout>
And my Routes looked like this:
<BrowserRouter>
<Switch>
<Route ...>
....
</Switch>
</BrowserRouter>
The useLocation() hook in my Layout wasn't working since it was not a child of my BrowserRouter. Changing it to be like that made the code work.
Only thing I find weird is that in my app, I didn't get an error for invalid hook call, but in the sandbox I did. Maybe I need to upgrade my React packages...