In react-router, is there any way to pass a property from the Route-definition that can be picked up within the Router.run function? I want specific actions to fire for specific routes. Something like this perhaps:
<Route handler={someComponent} resolve={someAction} />
In Router.Run i want to execute that given action defined in resolve. Is there any way of doing that?
The reason for doing this is to make sure that each route can instantiate service-calls (from a defined action) to make sure that the stores have the data needed for that route. Doing it in the component is also a possibility, the problem we have now is that several components within the same route needs the same data, making it requesting data from the API several times, and also triggering rerendering for each result comming in from those calls.
You can do something like this in React Router by adding a static (using createClass) or a property on the component class (when using ES6 classes) and then executing them with React Router.
var Component1 = React.createClass({
statics: fetchData: function(params) {
return API.getData(params);
},
// ...
});
class Component2 extends React.Component {
// ...
}
Component2.fetchData = (params) => API.getData(params);
Then, when you run your router, look for all matched routes that have a fetchData static method and call it. We'll assume here that fetchData (and thus API.getData) returns a Promise:
Router.run(routes, function(Root, state) {
var data = {};
var routesWithFetchData = state.routes.filter(function (route) {
return route.handler.fetchData
});
var allFetchDataPromises = routesWithFetchData.map(function (route) {
return route.handler.fetchData(state.params).then(function (routeData) {
data[route.name] = routeData;
});
});
Promise.all(allFetchDataPromises).then(function() {
React.render(<Root data={data} />, container);
});
});
(See this React Router example for more details.)
You can solve the "multiple components fetch the same data" problem by ensuring that the API module will catch requests for the same data and assign them the same promise. Pseudocode:
API = {
getData: function(params) {
if (requests[params]) {
return requests[params];
} else {
request[params] = new Promise(function(resolve, reject) {
// fetch data here
});
}
}
};
Related
I am fairly new to AngularJS (v 1.6) so this may be a dumb question.
I am trying to create a component that will be used on the home page of my application. No data will be passed to it, and instead I will make an API call.
Here is what it looks like thus far:
class MyInboxController {
constructor(api) {
this.api = api;
this.$onInit = () => {
this.count = this.api.getAllMessages().then(function(data) { data.length });
}
}
}
MyInboxController.$inject = ['api'];
export const myInboxComponent = {
template: require('./my-inbox.html'),
controller: MyInboxController
};
Essentially, this component will live on the home page of my application in the navigation. My problem is that when I call an api service I have in oninit, it never seems to get data back in time for the page to load. Any advice would be fantastic.
The code likely should be:
class MyInboxController {
constructor(api) {
this.api = api;
this.$onInit = () => {
api.getAllMessages().then( (data) => {
this.count = data.length
});
};
}
}
The .then method of returns a promise. The value from a promise needs to be extracted with a function provided to the .then method.
I am attempting to render a dynamic route preloaded with data fetched via an async thunk.
I have a static initialAction method in my Components that require preloading, and let them call the actions as needed. Once all actions are done and promises are resolved, I render the route/page.
The question that I have is: how do I reference the route parameters and/or props inside a static function?
Here is the relevant code that will call any initialAction functions that may be required to preload data.
const promises = routes.reduce((promise, route) => {
if (matchPath(req.url, route) && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store))))
}
return promise;
}, []);
Promise.all(promises)
.then(() => {
// Do stuff, render, etc
})
In my component, I have the static initialAction function that will take in the store (from server), and props (from client). One way or the other, the category should be fetched via redux/thunk. As you can see, I'm not passing the dynamic permalink prop when loading via the server because I'm unable to retrieve it.
class Category extends Component {
static initialAction(store, props) {
let res = store !== null ? getCategory(REACT_APP_SITE_KEY) : props.getCategory(REACT_APP_SITE_KEY, props.match.params.permalink)
return res
}
componentDidMount(){
if(isEmpty(this.props.category.categories)){
Category.initialAction(null, this.props)
}
}
/* ... render, etc */
}
And finally, here are the routes I am using:
import Home from '../components/home/Home'
import Category from '../components/Category'
import Product from '../components/product/Product'
import NotFound from '../components/NotFound'
export default [
{
path: "/",
exact: true,
component: Home
},
{
path: "/category/:permalink",
component: Category
},
{
path: "/:category/product/:permalink",
component: Product
},
{
component: NotFound
}
]
Not entirely sure I'm even doing this in a "standard" way, but this process works thus far when on a non-dynamic route. However, I have a feeling I'm waaaay off base :)
on the server you have request object and on the client, you have location object. Extract url parameters from these objects after checking current environment.
const promises = routes.reduce((promise, route) => {
var props = matchPath(req.url, route);
if ( obj && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store, props))))
}
return promise;
}, []);
now you will get url in initialAction in both desktop and server. you can extract dynamic route params from this
I have been trying to learn about being a full-stack web developer. I have decided to use the MERN stack. I am in the midst of trying to write my first "full-stack" application. However I can't seem to figure out how to store the data from my get call and submit it to the class as a property. The get call will reach an end point I have set up in nodejs which will make a call to Mongo and return an array of numbers. The get call below works as I can console.log the number of elements in that array. I have tried a number of different ways but I can't seem to figure out how to get the number out of the THEN promise and into my class to display on the screen. Any help would be greatly appreciated!
const React = require('react');
const ReactDOM = require('react-dom');
const axios = require('axios');
//call with npm build
var num = axios.get('/api').then(result => {
console.log(result.data.length)
return result.data.length;
})
//Only show how many unused coupons are left.
var Message = React.createClass({
render: function () {
return <h1>There are {this.props.number} coupons left!</h1>
}//end of of render outer function
})
ReactDOM.render(<Message number={num} />,document.getElementById('content'))
First, api call is an asynchronous operation, you can't call in a sync way.
And, instead of returning derived from api value, axios.get function returns Promise, which resolving this value.
Correct solution would be create <App /> component and call to api in componentDidMount lifecycle callback, set corresponding state variable on success response and render <Message /> component providing this variable to it.
Look at example:
var App = React.createClass({
getInitialState() {
return {
num: null
};
}
componentDidMount() {
var num = axios.get('/api').then(result => {
this.setState({ num: result.data.length });
});
},
render() {
if (this.state.num) {
return <Message number={this.state.num} />
}
return <div />
}
});
ReactDOM.render(<App />,document.getElementById('content'))
I'm using ReactJS, Redux (with server-side rendering) and react-router-redux as set up here and am getting a little thrown by how routes work with the rest of the redux state and actions.
For example, I have a members component with the route /members:
class Members extends Component {
static need = [
fetchMembers
]
render() {
...
the static need array specifies an action that populates an array on the state that is then mapped to the component props. That much works.
But then I have an individual member component with the route members/:memberId. How do I load that individual member in a way that works both client- and server-side.
What I'm doing now is the same:
class Member extends Component {
static need = [
fetchMembers
]
render() {
...
but then map just the single member
function mapStateToProps(state, ownProps) {
return {
member: state.member.members.find(member => member.id == ownProps.params.memberId),
};
}
This works but is obviously wrong. So the question is two-fold:
When the user clicks the router Link that has a query param (:memberId), how do I use that router param to query a specific document (assume a mongo database). Do I somehow trigger a separate action that populates an active member field on the redux state? Where does this happen, in the route component's componentDidMount?
How does this work with server-side rendering?
I’ve had the same question and seemed to find a way that works pretty well with my setup. I use Node, Express, React, React Router, Redux and Redux Thunk.
1) It really depends on where your data is. If the data needed for /member/:memberId is already in state (e.g. from an earlier call) you could theoretically filter through what you already have when componentDidMount is fired.
However, I'd prefer to keep things separate simply to avoid headaches. Starting to use one data source for multiple destinations/purposes throughout your app might give you long days down the road (e.g. when Component A needs more/less properties about the member than Component B or when Component A needs properties in a different format than Component B etc.).
This decision should of course be based on your use-case but due to the cost of API calls nowadays I wouldn't be afraid (at all) to make one when someone navigates to /member/:memberId.
2) I’ll answer with a simplified version of my typical setup:
Whenever a request comes through, I have this fella handle it.
// Imports and other jazz up here
app.use((req, res) => {
const store = configureStore({});
const routes = createRoutes(store);
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const fetchedData = renderProps.components
.filter(component => component.fetchData)
.map(component => component.fetchData(store, renderProps.params));
Promise.all(fetchedData).then(() => {
const body = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
res.status(200).send(`<!doctype html>${renderToStaticMarkup(
<Html
body={body}
state={store.getState()}
/>)
}`);
});
} else {
res.status(404).send('Not found');
}
});
});
It’ll look for fetchData on the components that are about to be rendered, and make sure we have the data before we send anything to the client.
On each and every route, I have a Container. The Container’s sole purpose is to gather the data needed for that route. As you’ve touched upon this can happen server-side (fetchData in my case) or client-side (componentDidMount in my case). A typical Container of mine looks like this:
// Imports up here
class Container extends Component {
static fetchData(store, params) {
const categories = store.dispatch(getCategories());
return Promise.all([categories]);
}
componentDidMount() {
this.props.dispatch(getCategoriesIfNeeded());
}
render() {
return this.props.categories.length ? (
// Render categories
) : null;
}
}
Container.propTypes = {
categories: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired,
params: PropTypes.object.isRequired,
};
function mapStateToProps(state) {
return {
categories: state.categories,
};
}
export default connect(mapStateToProps)(Container);
In the Container above I’m using getCategories and getCategoriesIfNeeded to make sure that I have the data needed for the route. getCategories is only called server-side, and getCategoriesIfNeeded is only called client-side.
Note that I have params available for both fetchData and componentDidMount (passed from connect()), which I could potentially use to extract something like :memberId.
The two functions used to fetch data above are listed below:
// Using this for structure of reducers etc.:
// https://github.com/erikras/ducks-modular-redux
//
// actionTypes object and reducer up here
export function getCategories() {
return (dispatch, getState) => {
dispatch({
type: actionTypes.GET_REQUEST,
});
return fetch('/api/categories').then(res => {
return !res.error ? dispatch({
error: null,
payload: res.body,
type: actionTypes.GET_COMPLETE,
}) : dispatch({
error: res.error,
payload: null,
type: actionTypes.GET_ERROR,
});
});
};
}
export function getCategoriesIfNeeded() {
return (dispatch, getState) => {
return getState().categories.length ? dispatch(getCategories()) : Promise.resolve();
};
}
As displayed above I have both dispatch and getState available thanks to Redux Thunk - that handles my promises too - which gives me freedom use the data I already have, request new data and do multiple updates of my reducer.
I hope this was enough to get you moving. If not don't hesitate to ask for further explanation :)
The answer, it turns out, was pretty simple. The implementation taken from Isomorphic Redux App ties the need static property on a component back to the router by passing the routes query params into the action creator.
So for the route:
items/:id
you'd use a component like
class Item extends Component {
static need = [
fetchItem
]
render() {
specifying that it needs the fetchItem action. That action is passed the route's query params, which you can use like
export function fetchItem({id}) {
let req = ...
return {
type: types.GET_ITEM,
promise: req
};
}
For a more detailed explanation about why this work, read marcfalk's answers, which describes a very similar approach.
I have a simple navigation bar use case where I have
Components
NavBar.jsx
NavBarItem.jsx
Stores
NavStore.js
Using Dependencies
React
Reflux
I'd render the navbar like
<NavBar active={itemName} itemList={itemList} />
The question is my initial state is {}, so is my NavStore, since there's no external data involved, how should I initialize the NavStore.js with itemList information?
I tried to add a helper method on Store like initializeData(data), and call it in NavBar Component's render or getInitialState(), it somehow is always getting called after a this.setState() on Component and reinitialized store with initial values. Not sure why getInitialState() always get called on this.setState().
I am also using Reflux as the action dispatcher but I am seeing the getInitialState() called on every action. It seems a bit odd. Does Reflux trigger a recreation of component by default?
Write an initialize function that initiates an action called INITIALIZE_APP and have the necessary stores perform initialization on receiving this action. Wait for all the stores to finish the initialization before rendering the root react component.
//initialize.js
var initialize = function() {
var dispatchToken = AppDispatcher.register(payload => {
var action = payload.action;
if (action.type !== AppConstants.ActionTypes.INITIALIZE_APP) {
return;
}
// wait for all the stores to initialize before rendering
var tokens = [
CustomerStore,
NavigationStore,
].map(store => store.dispatchToken);
AppDispatcher.waitFor(tokens);
AppDispatcher.unregister(dispatchToken);
});
InitializeAppActions.initialize(); // Creates INITIAL_LOAD action
};
module.exports = initialize;
Have an action defined for INITALIZE_APP
// InitializeAppActions.js
var InitializeAppActions = {
initialize() {
AppDispatcher.handleViewAction({
type: ActionTypes.INITIALIZE_APP,
});
return true;
},
};
module.exports = InitializeAppActions;
The store listens to the INITIALIZE_APP action
//CustomerStore.js
CustomerStore.dispatchToken = AppDispatcher.register(function(payload) {
var action = payload.action;
switch (action.type) {
//Called when the app is started or reloaded
case ActionTypes.INITIALIZE_APP:
initializeCustomerData();
break;
}
Call the initialize function before the executing the root react component.
//app.js
var initialize = require("./initialize.js");
//initialize the stores before rendering
initialize();
Router
.create({
routes: AppRoutes,
})
.run(function(Handler) {
React.render( <Handler/>, document.getElementById("react-app"));
});