React pass props to children edge case? - javascript

My app has the structure below and I would like to pass a prop based in the Header state to the RT component. I can pass it easily with Context but I need to call an API when the prop is a certain value and Context doesn't seem to be designed for this use due to it using the render pattern.
In Header.js I render the children with this.props.children. To pass props I've tried the following patterns but nothing is working. I'm missing a concept here. What is it?
(1) React.Children.map(children, child =>
React.cloneElement(child, { doSomething: this.doSomething }));
(2) {React.cloneElement(this.props.children, { loggedIn: this.state.loggedIn })}
(3) <Route
path="/issues"
render={({ staticContext, ...props }) => <RT {...props} />}
/>
Structure:
App.js
<Header>
<Main />
</Header>
Main.js
const Main = () => (
<Grid item xl={10} lg={10}>
<main>
<Switch>
<Route exact path="/" component={RT} />
<Route path="/projects" component={Projects} />
<Route path="/issues" component={RT}/>
<Route path="/notes" component={Notes} />
</Switch>
</main>
</Grid>
);

I would personally recommend using the React Context API to handle the user state rather than manually passing it via props. Here's an example of how I use it:
import React from 'react';
export const UserContext = React.createContext();
export class UserProvider extends React.Component {
constructor(props) {
super(props);
this.state = {
user: false,
onLogin: this.login,
onLogout: this.logout,
};
}
componentWillMount() {
const user = getCurrentUser(); // pseudo code, fetch the current user session
this.setState({user})
}
render() {
return (
<UserContext.Provider value={this.state}>
{this.props.children}
</UserContext.Provider>
)
}
login = () => {
const user = logUserIn(); // pseudo code, log the user in
this.setState({user})
}
logout = () => {
// handle logout
this.setState({user: false});
}
}
Then you can use the User context wherever you need it like this:
<UserContext.Consumer>
{({user}) => (
// do something with the user state
)}
</UserContext.Consumer>

Related

How do I turn a JSX Element into a Function Component in React?

My React app has the following in App.js:
const App = () => (
<Router>
<Switch>
... various routes, all working fine ...
<Route exact path={ROUTES.DASHBOARD} render={(props) => <Dashboard {...props} />}/>
</Switch>
</Router>
);
I'm getting an error on Dashboard, which says JSX element type 'Dashboard' does not have any construct or call signatures.
This is because Dashboard is created like this:
const DashboardPage = ({firebase}:DashboardProps) => {
return (
<div className="mainRoot dashboard">...contents of dashboard...</div>
);
}
const Dashboard = withFirebase(DashboardPage);
export default Dashboard;
and withFirebase is:
import FirebaseContext from './firebaseContext';
const withFirebase = (Component:any) => (props:any) => (
<FirebaseContext.Consumer>
{firebase => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
export default withFirebase;
So withFirebase is exporting a JSX element, so that's what Dashboard is. How can I ensure that withFirebase is exporting a Component instead?
So withFirebase is exporting a JSX element, so that's what Dashboard is. How can I ensure that withFirebase is exporting a Component instead?
withFirebase is not creating a JSX element, it is creating a function which creates a JSX Element -- in other words that's a function component. Perhaps it helps to type it properly.
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase | null }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
Those type are explained in detail in this answer. Is your context value sometimes null? Can your DashboardPage handle that, or do we need to handle it here? Here's one way to make sure that DashboardPage can only be called with a valid Firebase prop.
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) =>
firebase ? (
<Component {...props} firebase={firebase} />
) : (
<div>Error Loading Firebase App</div>
)
}
</FirebaseContext.Consumer>
);
Now that we have fixed the HOC, your Dashboard component has type React.FC<{}>. It's a function component that does not take any props.
You do not need to create an inline render method for your Route (this will actually give errors about incompatible props). You can set it as the component property component={Dashboard}.
complete code:
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
// placeholder
class Firebase {
app: string;
constructor() {
this.app = "I'm an app";
}
}
const FirebaseContext = React.createContext<Firebase | null>(null);
const withFirebase = <Props extends {}>(
Component: React.ComponentType<Omit<Props, "firebase"> & { firebase: Firebase }>
): React.FC<Props> => (props) => (
<FirebaseContext.Consumer>
{(firebase) =>
firebase ? (
<Component {...props} firebase={firebase} />
) : (
<div>Error Loading Firebase App</div>
)
}
</FirebaseContext.Consumer>
);
interface DashboardProps {
firebase: Firebase;
}
const DashboardPage = ({ firebase }: DashboardProps) => {
console.log(firebase);
return <div className="mainRoot dashboard">...contents of dashboard...</div>;
};
const Dashboard = withFirebase(DashboardPage);
const App = () => {
const firebase = new Firebase();
return (
<FirebaseContext.Provider value={firebase}>
<Router>
<Switch>
<Route component={Dashboard} />
</Switch>
</Router>
</FirebaseContext.Provider>
);
};
export default App;

PrivateRoute on React is not redirecting when loggedIn user is false

I'm trying to implement some security into my app and ended up creating this code:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const PrivateRoute = ({
component: Component,
auth: { isAuthenticated, loading },
...rest
}) => (
<Route
{...rest}
render={props =>
!isAuthenticated && !loading ? (
<Redirect to='/auth/login' />
) : (
<Component {...props} />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
auth comes from my state management that looks exactly like this when viewed from the Redux Devtools installed on my Chrome browser; here it is:
isAuthenticated and loading are usually true when a user is loggedIn; that works just fine. The problem I'm having is that my PrivateRoute does not redirect to the auth/login page when no one is loggedIn. Does anyone has any idea on how to fix this?. This is an example of one of my routes that need the PrivateRoute component:
<PrivateRoute exact path='/edit-basics' component={EditBasics} />
The route above is a page to edit the current loggedIn user info only available to him/her. I'm still accessing to it without being loggedIn.
So, you're probably getting errors from having the && operator and two expressions inside of the ternary operation that you're passing to the render method of Route.
You'll have to find a different way to validate if it's still loading.
In JSX if you pair true && expression it evaluates to expression – so basically you're returning !loading as a component to render.
Read more: https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator
const PrivateRoute = ({
component: Component,
auth: { isAuthenticated, loading },
...rest
}) => (
<Route
{...rest}
render={props =>
// TWO OPERATIONS WITH && WILL CAUSE ERROR
!isAuthenticated && !loading ? (
<Redirect to='/auth/login' />
) : (
<Component {...props} />
)
}
/>
);
Also,
React Router's authors recommend constructing this kind of private route with child components instead of passing the component to render as a prop.
Read more: https://reacttraining.com/react-router/web/example/auth-workflow
function PrivateRoute({ auth, children, ...rest }) {
return (
<Route
{...rest}
render={() =>
!auth.isAuthenticated ? (
<Redirect
to={{
pathname: "/auth/login",
state: { from: location }
}}
/>
) : (
children
)
}
/>
);
}
And to call that route:
<PrivateRoute path="/protected" auth={auth} >
<ProtectedPage customProp="some-prop" />
</PrivateRoute>
It looks like you are not passing down the auth prop to the PrivateRoute. Try adding it.
<PrivateRoute exact path='/edit-basics' component={EditBasics} auth={this.props.auth}/>
maybe something like this
const PrivateRoute = ({ component,
auth: { isAuthenticated, loading },
...options }) => {
let history = useHistory();
useLayoutEffect(() => {
if ( !isAuthenticated && !loading) history.push('/auth/login')
}, [])
return <Route {...options} component={component} />;
};

ReactJS Conditional Header with Redux

I want to create a conditional component called HeaderControl that can generate a type of Header for the aplication if the user is Logged or not.
This is my Header.jsx :
import React from 'react';
import { connect } from 'react-redux';
import { isAuthenticated } from '../Login/reducers';
import ucaLogo from '../../assets/logo.gif';
import '../../styles/base.scss';
const mapStateToProps = state => ({
isAuthenticated: isAuthenticated(state),
});
function HeaderNoLogin() {
return <div className="header-div col-md-12">
<img className="img-login-header" src={ucaLogo} alt="logo" />
<div className="title-head-div">
<p className="title-head">Not logged</p>
<p className="title-head"> Not logged</p>
</div>
</div>;
}
function HeaderLogged() {
return <div className="header-div col-md-12">
<img className="img-login-header" src={ucaLogo} alt="" />
<div className="title-head-div">
<p className="title-head">Logged</p>
<p className="title-head"> Logged</p>
</div>
</div>;
}
class HeaderControl extends React.Component {
render() {
const isLoggedIn = (props) => {
if (this.props.isAuthenticated) {
return true;
}
return false;
};
let button = null;
if (isLoggedIn) {
button = <HeaderLogged />;
} else {
button = <HeaderNoLogin />;
}
return (
<div>
{button}
</div>
);
}
}
export default connect(mapStateToProps)(HeaderControl);
My entry point (app.jsx) in have a Provider with the store like this:
const history = createHistory();
const store = configureStore(history);
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<MuiThemeProvider>
<Switch>
<Route path="/login" component={Login} />
<PrivateRoute path="/" component={Home} />
</Switch>
</MuiThemeProvider>
</ConnectedRouter>
</Provider>,
document.getElementById('app'),
);
My questions are:
Where and how can I check if the user is authenticate using the redux store?
Where and how should I import the Header ? I think that I should import it in the app.jsx but I do not know where exactly.
Sorry if these are dumbs questions but this is the first time that I am using Reactjs with Redux.
Thanks.
If your a newbie docs has good example in react-router v4 https://reacttraining.com/react-router/web/guides/philosophy.
index.js:
export const store = createStore(
app,
composeWithDevTools(applyMiddleware(thunk))
); //mandatory to use async in redux
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<Router>
<App />
</Router>
</ConnectedRouter>
</Provider>,
document.getElementById("root")
);
And in app.js:
const App = () => (
<div>
<HeaderControl /> // header will be always visible
<MuiThemeProvider>
<Switch>
<Route path="/login" component={Login} />
<PrivateRoute path="/" component={Home} />
</Switch>
</MuiThemeProvider>
</div>
);
export default App;
Handle async logic with redux is not intuitive you shouldn't start by that if your new to redux.
reducers.js:
import {LOGGED_IN, LOGGED_OUT} from '../actions/User'
const initialState = {
logged: false
}
// reducer
function User (state = initialState, action) {
switch (action.type) {
case LOGGED_IN:
return Object.assign({}, state, {logged: true}, action.payload)
case LOGGED_OUT:
return initialState
default:
return state
}
}
export default User
You neet to setup a middlewear to handle async request in redux. It is the more simple approach. there is others options like redux saga and redux-observable. In this example i use redux-thunk.
I advise you to check redux docs on middlweare
https://redux.js.org/docs/advanced/Middleware.html
and/or this course on udemy https://www.udemy.com/react-redux-tutorial/
export const checkLogin = payload => {
return dispatch =>
xhr
.post(/login , payload) // payload is json name and password
.then(res => dispatch(LOGGED_IN))) // response is positive
.cath(err => console.log(err))
}
And your checkLogin action should be using in login.js form
login.js
import checkLogin from action.js
class Login extends React.Component {
render() {
return (
<form submit={checkLogin}>
{" "}
// this is pseudo code but i hope it's clear , you take data from your
form and you you call cheklogin.
</form>
);
}
}
So the logic is pretty simple:
In login you call the action checkLogin
A request will start to you server with login+pass
When the request end login reducer is call.
Reducer update state.
Header is refresh.

React js: can I pass data from a component to another component?

I'm new to React and I'm still learning it. I'm doing a personal project with it.
Let me explain my problem:
I have a component called <NewReleases /> where I make an ajax call and take some datas about some movies out on cinemas today. (I take title, poster img, overview etc...) I put all the data in <NewReleases /> state, so that state becomes an object containing an object for each movie and each object contains title poperty, poster property etc... Then I render the component so that it looks like a grid made by movies posters, infos and so on. And this works well.
Then I need a component <Movie /> to take some datas from the state of <NewReleases /> and render them on the HTML. I read other questions where people were having a similar problem, but it was different because they had a children component that was rendered by the parent component. And in that way, people suggested to pass state as props. I can't do that because my <Movie /> component is not rendered by <NewReleases />. <NewReleases /> makes the ajax call and only renders a JSX grid based on the retrieved data.
On index.js I have setup the main page this way:
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter, Switch, Route} from 'react-router-dom';
import {Home} from './home';
import {Movie} from './movie';
import './css/index.css';
class App extends React.Component {
render() {
return(
<BrowserRouter>
<Switch>
<Route path={'/movie/:movieTitle'} component={Movie} />
<Route path={'/'} component={Home} />
</Switch>
</BrowserRouter>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
(You can't see <NewReleases /> here because it is rendered inside of <Home /> component, which also renders a header and a footer.)
So when I click on a movie rendered by <NewReleases />, the app will let me go on localhost:3000/movie/:movieTitle where :movieTitle is a dynamic way to say the title of the movie (so for example if I click the poster of Star Wars rendered by <NewReleases />, I will go on localhost:3000/movie/StarWars). On that page I want to show detailed infos about that movie. The info are stored in <NewReleases /> state but I can't have access to that state from <Movie /> (I guess).
I hope you got what I want to achieve. I don't know if it is possible. I had an idea: on the <Movie /> I could do another ajax call just for the movie that I want but I think it would be slower and also I don't think it would be a good solution with React.
Note that I'm not using Redux, Flux etc... only React. I want to understand React well before to move to other technologies.
The way you wanna do is more complicated. With parents componentes that's easy to do. And with Redux is much more easy.
But, you wanna this way. I think if you have a state in the app, pass to home a props to set a movie-state and pass this movie-state to component Move, works fine.
The problem is that Route does't pass props. So there is a extensible route you can do. In the code below I get from web this PropsRoute.
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter, Switch, Route} from 'react-router-dom';
import {Home} from './home';
import {Movie} from './movie';
import './css/index.css';
class App extends React.Component {
constructor() {
super();
this.state = {
movie: {}
}
this.setMovie = this.setMovie.bind(this);
}
setMovie(newMovie) {
this.setState({
movie: newMovie
});
}
render() {
return(
<BrowserRouter>
<Switch>
<PropsRoute path={'/movie/:movieTitle'} movie={this.state.movie} component={Movie} />
<PropsRoute path={'/'} component={Home} setMovie={this.setMovie} />
</Switch>
</BrowserRouter>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
-----
const renderMergedProps = (component, ...rest) => {
const finalProps = Object.assign({}, ...rest);
return (
React.createElement(component, finalProps)
);
}
const PropsRoute = ({ component, ...rest }) => {
return (
<Route {...rest} render={routeProps => {
return renderMergedProps(component, routeProps, rest);
}}/>
);
}
I thinks this can solve your problem.
Here's a quick example. Store your state in a parent component and pass down the state down to your components. This example uses React Router 4, but it shows how you can pass down the setMovie function and movie information via state to one of your child components. https://codepen.io/w7sang/pen/owVrxW?editors=1111
Of course, you'll have to rework this to match your application, but a basic run down would be that your home component should be where you're grabbing your movie information (via AJAX or WS) and then the set function will allow you to store whatever information you need into the parent component which will ultimately allow any child components to access the information you have stored.
const {
BrowserRouter,
Link,
Route,
Switch
} = ReactRouterDOM;
const Router = BrowserRouter;
// App
class App extends React.Component{
constructor(props) {
super(props);
this.state = {
movie: {
title: null,
rating: null
}
};
this.setMovie = this.setMovie.bind(this);
}
setMovie(payload) {
this.setState({
movie: {
title: payload.title,
rating: payload.rating
}
});
}
render(){
return(
<Router>
<div className="container">
<Layout>
<Switch>
<Route path="/select-movie" component={ () => <Home set={this.setMovie} movie={this.state.movie} />} />
<Route path="/movie-info" component={()=><MovieInfo movie={this.state.movie}/>} />
</Switch>
</Layout>
</div>
</Router>
)
}
}
//Layout
const Layout = ({children}) => (
<div>
<header>
<h1>Movie App</h1>
</header>
<nav>
<Link to="/select-movie">Select Movie</Link>
<Link to="/movie-info">Movie Info</Link>
</nav>
<section>
{children}
</section>
</div>
)
//Home Component
const Home = ({set, movie}) => (
<div>
<Movie title="Star Wars VIII: The Last Jedi (2017)" rating={5} set={set} selected={movie} />
<Movie title="Star Wars VII: The Force Awakens (2015)" rating={5} set={set} selected={movie} />
</div>
)
//Movie Component for displaying movies
//User can select the movie
const Movie = ({title, rating, set, selected}) => {
const selectMovie = () => {
set({
title: title,
rating: rating
});
}
return (
<div className={selected.title === title ? 'active' : ''}>
<h1>{title}</h1>
<div>
{Array(rating).fill(1).map(() =>
<span>★</span>
)}
</div>
<button onClick={selectMovie}>Select</button>
</div>
)
}
//Movie Info
//You must select a movie before movie information is shown
const MovieInfo = ({movie}) => {
const {
title,
rating
} = movie;
//No Movie is selected
if ( movie.title === null ) {
return <div>Please Select a Movie</div>
}
//Movie has been selected
return (
<div>
<h1>Selected Movie</h1>
{title}
{Array(rating).fill(1).map(() =>
<span>★</span>
)}
</div>
)
}
ReactDOM.render(<App />,document.getElementById('app'));
nav {
margin: 20px 0;
}
a {
border: 1px solid black;
margin-right: 10px;
padding: 10px;
}
.active {
background: rgba(0,0,0,0.2);
}
<div id="app"></div>
Create a manual object store to get/set the movie information and use it. That's it. Try the following code. That should answer all your questions. Click on any of the new releases, it will redirect to movie info screen with all the details. If you feel bad about the new releases data always refreshing, you may have to create another store, then get/set the data by checking the data exist in store.
Note: Using store and using title(duplicates may occur) in browser URL makes some problems when user refreshes the browser. For that, use id in browser URL, fetch the details using AJAX call and set that details in store.
//store for movie info
const movieInfoStore = {
data: null,
set: function(data) {
this.data = data;
},
clear: function() {
this.data = null;
}
};
class MovieInfo extends React.Component {
componentWillUnmount() {
movieInfoStore.clear();
}
render() {
return (
<div>
<pre>
{movieInfoStore.data && JSON.stringify(movieInfoStore.data)}
</pre>
<button onClick={() => this.props.history.goBack()}>Go Back</button>
</div>
)
}
}
MovieInfo = ReactRouterDOM.withRouter(MovieInfo);
class NewReleases extends React.Component {
handleNewReleaseClick(newRelease) {
movieInfoStore.set(newRelease);
this.props.history.push(`/movie/${newRelease.title}`);
}
render() {
const { data, loading } = this.props;
if(loading) return <b>Loading...</b>;
if(!data || data.length === 0) return null;
return (
<ul>
{
data.map(newRelease => {
return (
<li onClick={() => this.handleNewReleaseClick(newRelease)}>{newRelease.title}</li>
)
})
}
</ul>
)
}
}
NewReleases = ReactRouterDOM.withRouter(NewReleases);
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
newReleases: [],
newReleasesLoading: true
};
}
componentDidMount() {
setTimeout(() => {
this.setState({
newReleases: [{id: 1, title: "Star Wars"}, {id: 2, title: "Avatar"}],
newReleasesLoading: false
});
}, 1000);
}
render() {
const { newReleases, newReleasesLoading } = this.state;
return (
<NewReleases data={newReleases} loading={newReleasesLoading} />
)
}
}
class App extends React.Component {
render() {
const { BrowserRouter, HashRouter, Switch, Route } = ReactRouterDOM;
return (
<HashRouter>
<Switch>
<Route path="/movie/:movieTitle" component={MovieInfo} />
<Route path="/" component={Home} />
</Switch>
</HashRouter>
)
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://unpkg.com/react-router-dom/umd/react-router-dom.min.js"></script>
<div id="root"></div>

Passing states to this.props.children using Nested Routes

I agree there are multiple questions asked for the same. I did a lot of research for a few hours, however, I couldnt resolve this easy looking error. According to How to pass props to {this.props.children} post, i understand the easy use of React.cloneElement and React.Children.
Following is my Parent Class:
class AboutPage extends React.Component {
constructor(props, context){
super(props, context);
this.state = {
details: "details"
}
}
render() {
const childrenWithProps = React.Children.map(this.props.children,
(child) => {
React.cloneElement(child, {
details: this.state.details
})
}
);
return (
<div className="jumbotron">
<h1>About</h1>
<p>This application uses React, Redux, React Router and other libs for our EducationOne Project</p>
<Link to="/about/Owner">
<Button color="primary">test</Button>
</Link>
{childrenWithProps}
</div>
);
}
}
AboutPage.PropTypes = {
children: PropTypes.object.isRequired
};
Following is my child component:
const Owner = (props) => {
return (
<div>Owner details: {props.details}</div>
);
};
Instead of importing the child or parent, i nested the route in my routes.js to create a child for aboutPage:
export default (
<Route path="/" component={App}>
<IndexRoute component={Login} />
<Route path="home" component={HomePage}/>
<Route path="about" component={AboutPage}>
<Route path="omkar" components={Omkar} />
</Route>
<Route path="courses" component={CoursesPage}>
{/*<IndexRoute components={CourseDetailsAndList}/>*/}
</Route>
</Route>
);
However, I dont see any error or any message in the console, nor the child component loaded when i click the button to load the child component.
Any help will be really appreciated.
The problem is in your map function. The callback arrow function has block body with brackets and so you need to explicitly return your cloned element with the return keyword.
In a concise body, only an expression is needed, and an implicit
return is attached. In a block body, you must use an explicit return
statement.
const childrenWithProps = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
details: this.state.details
});
});

Categories

Resources