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

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.

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>

ReactJS, React-Router: Calling parent function

I am working on a React App, trying to call a parent method from a child component, some code of the parent component below:
class NavigationBar extends Component {
constructor(props){
super(props);
this.state={isLoggedIn: false};
}
updateLoginState(){
alert("Login from NavigationBar");
}
GetBar() {
//const isLoggedIn = this.props.isLoggedIn;
if (false){ //isLoggedIn
return this.UserNavBar();
}
return this.StrangerNavBar();
}
StrangerNavBar(){
return (
<div>
<HashRouter>
<div>
{/* ... */}
<div className="content">
<Route exact path="/LoginCC" loginUpdate={this.updateLoginState} component={LoginCC} />
</div>
</div>
</HashRouter>
</div>
);
}
render() {
return (
this.GetBar()
);
}
}
export default NavigationBar;
This component is supposed to redirect the user to different content pages based on whether or not he is logged in, using a Router. If a button is clicked in LoginCC.js the method updateLoginState should be invoked which just displays a message for now. The child content page LoginCC.js looks as follows:
class LoginCC extends Component {
constructor(props){
super(props);
this.state = {isLoggedIn: false};
}
render() {
return (
<div>
<HashRouter>
{/* ... */}
<Button variant="primary" size="lg" block onClick={this.props.loginUpdate}>
Log in
</Button>
{/* ... */}
</HashRouter>
</div>
);
}
}
export default LoginCC;
I passed the method reference as a prop to LoginCC when rendering this component using the Router, so a message should pop up if I press the button, but nothing happens.
Am I passing the prop incorrectly or something else I've missed? I'm new to React so any help is appreciated.
Route doesn't pass any custom props to components. You should use other method to pass functions.
One of solutions is:
<Route exact path="/LoginCC" render={
props => <LoginCC {...props} loginUpdate={this.updateLoginState}/>
} />
Note that updateLoginState will not get this when called. You should either bind it or declare it as an arrow function to get the correct this.
Also check the Context documentation.

Passing data from one page to another while using router and state in ReactJS

I am new to ReactJS. I am trying to figure out how to pass data from one page to another while using router. When I click on Add User button, I want to add the user and navigate back to home page.
I've searched the web for this and found examples where data is transferred from parent to child using props. But here I am using router. How can I find a way to do this?
index.js
ReactDOM.render(
<Router history={browserHistory}>
<Route path="/" component={Home}>
<IndexRoute component={AllUsers}/>
</Route>
<Route path="/add" component={Add}/>
</Router>
, document.getElementById('root'));
Home.js:
class Home extends Component {
render() {
return (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/add">Add</Link></li>
</ul>
</nav>
<div>
{this.props.children}
</div>
</div>
);
}
}
AllUsers.js
class AllUsers extends Component {
constructor(props) {
super(props);
this.state = {
headingsData: ["Name"],
rowsData: []
}
}
componentDidMount() {
this.setState({
rowsData: [{name: "Alex"}, {name: "Jack"}]
})
}
render() {
return (
<div className="allUsersContainter">
<h2>All users</h2>
<table border="1" className="tableComp">
<Headings headings={this.state.headingsData}/>
<tbody>
<Rows rows={this.state.rowsData}/>
</tbody>
</table>
</div>
);
}
}
class Headings extends Component {
render() {
let headings = this.props.headings.map((heading, i) => {
return (<th>{heading}</th>);
});
return (<thead><tr>{headings}</tr></thead>);
}
}
class Rows extends Component {
render() {
let rows = this.props.rows.map((row, i) => {
return (<tr key={i}><td>{row.name}</td></tr>);
});
return (<tbody>{rows}</tbody>);
}
}
export default AllUsers;
Add.js
class Add extends Component {
render() {
return (
<form>
<label>First Name:</label><input placeholder="Name" ref="name" required/><br/>
<button type="submit">Add user</button>
</form>
);
}
}
export default Add;
This is a common problem to share a component state with another (not a child) component.
A good solution, in this case, is to use a state management library. Check out Redux or MobX as they are very common with React applications.
The main benefits of using those libraries with React application is Easy state sharing. This is the most important for you because you can share your state not only with child components as a props but almost for every component in your application.
For easy understanding, you can imagine state management libraries as a global state for your application, which you can modify almost from every component and subscribe to it changes.

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.

Passing part of an url to another function in React

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.

Resources