Passing part of an url to another function in React - reactjs

Is there a way for React to take the back of an url user entered and pass it to a function? for example: www.site.com/foobar will pass the foobar to a function.
Essencially what i'm trying to do is to run a check on foobar being in my database inside the checker function, if not there display 404 page not found.
const NotFound = () => (<h1>404.. This page is not found!</h1>)
class App extends Component {
checker : function(e){
if(foobar exists)
//load page with data
else
// {NotFound}
}
render() {
return (
<Router history={hashHistory}>
<Route path='/' component={LoginPage} />
<Route path='*' component={this.checker()} />
</Router>
)
}
}

To expand what I had written in my comment -
class App extends React.Component {
render() {
return (
<Router history={hashHistory}>
<Route path='/' component={LoginPage} />
<Route path='/:token' component={EmailValidation} />
</Router>
)
}
}
class EmailValidation extends React.Component {
state = { checked: false, valid: false }
componentDidMount = () => {
checkToken(this.props.params.token).then((valid) => {
this.setState({ checked: true, valid })
})
}
render() {
const { checked, valid } = this.state
return (
<div>
{ checked
? <div>{ valid ? 'valid' : 'invalid' }</div>
: <div>Checking token...</div> }
</div>
)
}
}
this would be a good use case for an HoC which conditionally renders either the component you want or a 404 page - it would remove the binding between the 404 page and the email validate component (which are only sorta-kinda related)
if you're into using libraries, recompose has a bunch of nice helpers which can accomplish something like this for you.
something else you can do is use react-router's onEnter callback/prop although, iirc, you can't directly access props from that callback.

Related

React router is not mounting the component

I'm using React Router for routing to different routes as below:
<Router>
<Switch>
<Route path="/teams/:teamName/matches" >
<MatchPage/>
</Route>
<Route path="/teams/:teamName" >
<TeamPage/>
</Route>
</Switch>
</Router>
Now in my TeamPage component I'm calling an API using async and then in the render method invoking another component called MatchDetailCard
class TeamPage extends Component {
constructor(props) {
console.log('const called')
super(props)
this.state = {
team: [],
teamName:null
}
}
async componentDidMount() {
console.log(this.props.match.params.teamName);
const teamName = this.props.match.params.teamName;
const response = await fetch(`http://localhost:8080/team/${teamName}`);
const json = await response.json();
console.log(json);
this.setState({team:json, teamName: teamName});
}
componentDidUpdate () {
console.log('updated')
}
render() {
if (!this.state.team || !this.state.team.teamName) {
return <h1>Team not found</h1>;
}
return (
<div className="TeamPage">
<div className="match-detail-section">
<h3>Latest Matches</h3>
<MatchDetailCard teamName={this.state.team.teamName} match={this.state.team.matches[0]}/>
</div>
</div>
);
}
}
export default withRouter(TeamPage);
Within the MatchDetailCard component I create a router Link to the same TeamPage component which will have a different teamName this time as below:
const MatchDetailCard = (props) => {
if (!props.match) return null;
const otherTeam = props.match.team1 === props.teamName ? props.match.team2 : props.match.team1;
const otherTeamRoute = `/teams/${otherTeam}`;
const isMatchWon = props.teamName === props.match.matchWinner;
return (
<div className={isMatchWon ? 'MatchDetailCard won-card' : 'MatchDetailCard lost-card'}>
<div className="">
<span className="vs">vs</span><h1><Link to={otherTeamRoute}>{otherTeam}</Link></h1>
</div>
</div>
);
}
export {MatchDetailCard};
The problem that I'm facing is that the on-click of the link to /team/teamName route only the TeamPage component is not mounting instead it's just getting an update.
I want to have the call to componentDidMount hook to make the API call again in this scenario.
What's the problem with my logic?
If the same component is used as the child of multiple <Route>s at the same point in the component tree, React will see this as the same component instance and the component’s state will be preserved between route changes. If this isn’t desired, a unique key prop added to each route component will cause React to recreate the component instance when the route changes.
https://reactrouter.com/web/api/Route
You can add the teamName as a key prop on the component, which will tell React to unmount/mount the component when the key value changes.
<Router>
<Switch>
<Route
path="/teams/:teamName/matches"
render={({ match }) => {
return <MatchPage key={match.params.teamName} />;
}}
/>
<Route
path="/teams/:teamName"
render={({ match }) => {
return <TeamPage key={match.params.teamName} />;
}}
/>
</Switch>
</Router>

Single source of truth for all React child components with flux?

I'm playing around with Flux and it's working great but I'm now trying to use it in a way where a flux store determines a single point of truth or state for all child components.
I'm basically trying to implement an auth system, which determines the auth state of the current user without having to update the state of each component individually (there are lots of them) by doing something like this :
state = {
authenticated: AuthStateStore.getState(),
uid: AuthStateStore.getUid(),
token: AuthStateStore.getToken(),
username: AuthStateStore.getUsername(),
avatar: AuthStateStore.getAvatar(),
errors: []
}
I thought I could set the state on the main parent <App/> component (from which everything else is rendered as children) and then pass the state down as props to all children. This indeed works - {this.props.state.authenticated} will show the correct state from <App/> in the children - but props are immutable, meaning that when the state of <App /> is updated via a flux store, none of the props being sent to children are updated with the new values from <App/>.
Is there any way to achieve what I'm trying to do here or do I have to set the state from the flux store in every component that requires the info in this way?
EDIT: how I'm passing the state down from <App/> (I've trimmed the fat so they are more readable).
App (Main parent)
class App extends Component {
state = {
authenticated: AuthStateStore.getState(),
uid: AuthStateStore.getUid(),
token: AuthStateStore.getToken(),
username: AuthStateStore.getUsername(),
avatar: AuthStateStore.getAvatar()
}
render() {
return (
<BrowserRouter>
<div>
<Sidebar state={this.state}/>
<Switch>
<Route exact path="/" component={Home} state={this.state}/>
<Route exact path="/signup" component={Signup} state={this.state}/>
<Route path="/activate/([\da-f]+)" component={Activate}/>
.......
</Switch>
<Footer />
</div>
</BrowserRouter>
);
}
}
Sidebar (first child)
class Sidebar extends Component {
render() {
return (
<div className="sidebar">
<Navigation state={this.props.state}/>
<Search/>
</div>
);
}
}
Navigation (Second child)
class Navigation extends Component {
render() {
if (this.props.state.authenticated === "true") {
return (
<div>
<p>Authenticated content here</p>
</div>
);
} else {
return (
<div>
<p>Non-authenticated content here</p>
</div>
);
}
}
}
The above code works on first load, but if the state of <App/> changes via the flux store, this isn't reflected in the children unless you do a full page reload (which we obviously don't want to do).
Further Edit...
Just to be concise, right now I have everything working by doing the following in each child component that requires the Auth state info (again fat trimmed for readability)...
class Navigation extends Component {
state = {
authenticated: AuthStateStore.getState(),
uid: AuthStateStore.getUid(),
token: AuthStateStore.getToken(),
username: AuthStateStore.getUsername(),
avatar: AuthStateStore.getAvatar()
}
componentWillMount() {
AuthStateStore.on("change", () => {
this.setState({
authenticated: AuthStateStore.getState(),
uid: AuthStateStore.getUid(),
token: AuthStateStore.getToken(),
username: AuthStateStore.getUsername(),
avatar: AuthStateStore.getAvatar()
});
});
}
render() {
if (this.state.authenticated === "true") {
return (
<div>
<p>Authenticated content here</p>
</div>
);
} else {
return (
<div>
<p>Non-authenticated content here</p>
</div>
);
}
}
}
... but in the spirit of DNRY, and trying to avoid any potential pitfalls due to forgetting to call or set a state in a component somewhere, I'm looking for an alternative 'one-stop-shop' solution which I was hoping flux could provide.

hide id or query string while passing through react router

i am having route where i pass id,but i dont want to show id in url,
`<Route path={`${match.url}invite-members/:groupID`} exact component={InviteMembers} />`
this gets converted in url https://local..../invite-members/5,
but instead of that i want https://local..../invite-members, but the functionality should remain the same as in i get id in invite-members through this.props.match.params.groupID should be as it is,please help
using react router "react-router-dom": "^4.2.2",
If you want to change url to '/invite-members', you can add the Redirect component. And in case you want to save groupId, you could save it to your component state:
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import {
Router,
Route,
Link,
Switch,
Redirect
} from "react-router-dom";
class Root extends PureComponent {
// add groupId field to your component
// In case you use redux or any another state management library, you can save groupId to store
state = { groupId: null };
render() {
const { store, history } = this.props;
// just for example I defined '/' path to redirect on /invite-members url
return (
<Router>
<Switch>
<Route
path="/"
exact
render={props => (
<Redirect
to={{
pathname: "/invite-members/123",
state: { from: props.location }
}}
/>
)}
/>
<Route
path="/invite-members"
exact
render={props => (
<InviteMembers {...props} groupId={this.state.groupId} />
)}
/>
<Route
path="/invite-members/:groupID"
exact
render={props => {
return (
<RedirectAndSaveGroupId
{...props}
groupId={props.match.params.groupID}
onSetGroupId={groupId => {
this.setState({ groupId });
}}
/>
);
}}
/>
</Switch>
</Router>
);
}
}
export default Root;
class RedirectAndSaveGroupId extends PureComponent {
componentDidMount() {
// save groupId to Root component
this.props.onSetGroupId(this.props.groupId);
}
render() {
// redirect to /invite-members without groupId
return (
<Redirect
to={{
pathname: "/invite-members",
state: { from: this.props.location }
}}
/>
);
}
}
// Just for demo. In this.props.groupId we can receive groupId
class InviteMembers extends PureComponent {
render() {
return this.props.groupId;
}
}
Note, that in case you using any state management library such as Redux, you can store group id in them
I maybe have a very simple solution :
Router link :
<Link to={{pathname: '/item/'+name, state : {id}}}>{name}</Link>
In the Targeted file :
state = this.props.location.state
QueryParameters = () => {
const id = this.state.id
return { id }
}
And launch your query requiring the ID. It does not appear in the url.
Passing data in the params object will always result in that data being shown in the URL. Because the params object is built from the url.

How do I do authorization check on ComponentWillMount?

My website have a few pages that is protected by login. My current solution to this is:
in app.js:
<div className="app">
<Provider store={store}>
<Router history={appHistory} onUpdate={fireTracking}>
<Route name="main" component={AppHandler}>
<Route name="home" path="/" component={HomePageHandler}/>
</Route>
</Router>
</Provider>
</div>
And then my HomePageHandler is:
export default class HomePageHandler extends BaseAuthorizedComponent {
render() {
return (
<div>hello</div>
)
}
}
As the HomePageHandler extends BaseAuthorizedComponent, which is defined as:
class BaseAuthorizedComponent extends Component {
componentWillMount() {
if (!this.props.user.signed_in) {
this.context.router.push('/signin')
}
}
}
HomePageHandler.contextTypes = {
router: React.PropTypes.object.isRequired,
}
function select(state) {
return {
user: state.user,
}
}
export default connect(select)(BaseAuthorizedComponent)
The redux's user object has a flag that indicates if the user is logged in or not. The idea is that on the homepage, before the component is mounted, the BaseAuthorizedComponent would have checked and redirect to signin page if user is not logged in. My idea is to let every page that requires authorization to extend BaseAuthorizedComponent.
However the following error happens when trying to load the homepage:
Error: Could not find "store" in either the context or props of "Connect(BaseAuthorizedComponent)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(BaseAuthorizedComponent)".
No idea how can I fix the problem while keeping the advantage of a single place to check authorization. Any thoughts? Thanks!
First of all you is better to use composition instead of inheritance https://reactjs.org/docs/composition-vs-inheritance.html
Next, you can add "push" action creator from react-router-redux (https://github.com/reactjs/react-router-redux) to mapDispatchToProps function:
function composeAuth = (ComposedComponent) => {
class BaseAuthorizedComponent extends React.Component {
// We use componentDidMount instead of componentWillMount, cause componentWillMount is deprecated https://medium.com/#baphemot/whats-new-in-react-16-3-d2c9b7b6193b
componentDidMount() {
if (!this.props.user.signed_in) {
this.props.push('/signin');
}
}
render() {
if (!this.props.user.signed_in) {
return null;
}
return <ComposedComponent {...this.props} />
}
}
return connect(state => ({user: state.user}), {push})(BaseAuthorizedComponent);
}
class HomePageHandler extends React.Component {
render() {
return (
<div>hello</div>
)
}
}
export default composeAuth(HomePageHandler);
How about this:
class CheckAuth extends React.Component{
state = {
auth: false
}
render(){
return(
{this.state.auth ? <div>Authorized user</div> : <div>Unauthorized user</div>}
)
}
}
function mapStateToProps(state){
return{
auth: state.auth
}
}
export default connect(mapStateToProps)(CheckAuth);
And then include it in your other components like so:
import CheckAuth from './CheckAuth';
...
class Home extends React.Component{
render(){
return(
<div>
<CheckAuth />
Hello world!!
</div>
)
}
}
export default Home;
After more research, the easiest way to satisfy my requirement is:
in an util file:
export function requireAuth(nextState, replace) {
// use your own method to check if user is logged in or not
if (!isLoggedIn()) {
replace({pathname: '/signin'});
}
}
and then import this method in the app.js file and use it:
<div className="app">
<Provider store={store}>
<Router history={appHistory} onUpdate={fireTracking}>
<Route name="main" component={AppHandler}>
<Route name="home" path="/" component={HomePageHandler} onEnter={requireAuth}/>
</Route>
</Router>
</Provider>
</div>
In this way if the user requires auth(isLoggedIn() is false), then it will redirect the page to /signin.
after some research, the best way I've seen is this:
<Route name="name"
path="/path"
component={THeWorkHandler}
onEnter={requireAuth}/>
And the requireAuth is put in a helper file:
export function requireAuth(nextState, replace) {
if (!(//logic to see if user is logged in )) {
replace({pathname: '/user/signin'});
}
}
This way if a onEnter requireAuth determines that the user is not authenticated, it will redirect to /user/signin page.

React Router causes Redux container components re-render unneccessary

Here is my major code, App component is connected to Redux's store:
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostList} />
<Route path="/login" component={Login} />
<Route path="/topics" component={PostList} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
requestQuantity: getRequestQuantity(state)
};
};
export default connect(mapStateToProps)(App);
PostList component is also connected to Redux's store:
class PostList extends Component {
componentDidMount() {
this.props.fetchAllPosts();
}
render() {
const { posts} = this.props;
return (
// ...
);
}
//...
}
const mapStateToProps = (state, props) => {
return {
posts: getPostList(state),
};
};
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators(postActions, dispatch),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
When this.props.fetchAllPosts() is called, the requestQuantity in the global state will change from 0 to 1 (request starts) then to 0 (request ends). So the App will re-render twice. However, every re-rendering of App also causes PostList to re-render, which is what I don't expect, since PostList only depends on posts in the global state and posts doesn't change in these twice re-rendering.
I check React Router's source code and find the Route's componentWillReceiveProps will always call the setState, which set a new match object:
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
)
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
)
//the official always set a new match object ignoring whether the nextProps change or not
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
})
}
It is the new match prop passed to the PostList causing the Redux's shallow comparison fails and re-rendering occurs. I hope React Router's team can do some easy logic before setState, such as using (===) comparing every prop in nextProps and this.props, if no change occurs, skip setState. Unfortunately,they think it is not a big deal and closed my issue.
Now my solution is creating a HOC :
// connectRoute.js
export default function connectRoute(WrappedComponent) {
return class extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.location !== this.props.location;
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Then use connectRoute to wrap the containers used in Route:
const PostListWrapper = connectRoute(PostList);
const LoginWrapper = connectRoute(Login);
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostListWrapper} />
<Route path="/login" component={LoginWrapper} />
<Route path="/topics" component={PostListWrapper} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
Besides, when React Router is used with Mobx, this issue is also easy to meet.
Hope someone could offer better solutions. A long question. Thanks for your patience.

Resources