How to update the url of a page using an input field? - reactjs

I try to integrate a search page in my application with react router v5.
How could I update my url's query parameter using my search box?
When I refresh my application, I lose my search results and the value of my search field.
I use redux to manage the state of the value of my search fields and my search results, I think that going through the parameters of the url would be a better solution but I do not know how to do that.
I tried a solution (see my code), but the query parameter of the url is not synchronized with the value of my text field
Edit:
My component Routes.js
const Routes = (props) => {
return (
<div>
<Route exact path="/" component={Home} />
<Route
exact
path='/search'
component={() => <Search query={props.text} />}
/>
<Route path="/film/:id" component={MovieDetail} />
<Route path="/FavorisList" component={WatchList} />
<Route path="/search/:search" component={Search} />
<Route path="*" component={NotFound} />
</div>
)}
My component SearchBar.js (Embedded in the navigation bar, the Search route displays the search results)
EDIT:
I wish to realize the method used by Netflix for its research of series.
I want to be able to search no matter what page I am in, if there is an entry in the input field, I navigate to the search page withthis.props.history.push (`` search / ), if the input field is empty, I navigate to the page with this.props.history.goBack ().
The state inputChange is a flag that prevents me from pushing to the search page each time I enter a character.
To know more, I had opened = a post here => How to change the route according to the value of a field
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
inputValue:'',
};
}
setParams({ query }) {
const searchParams = new URLSearchParams();
searchParams.set("query", query || "");
return searchParams.toString();
}
handleChange = (event) => {
const query = event.target.value
this.setState({ inputValue: event.target.value})
if (event.target.value === '') {
this.props.history.goBack()
this.setState({ initialChange: true })
return;
}
if(event.target.value.length && this.state.initialChange){
this.setState({
initialChange:false
}, ()=> {
const url = this.setParams({ query: query });
this.props.history.push(`/search?${url}`)
this.search(query)
})
}
}
search = (query) => {
//search results retrieved with redux
this.props.applyInitialResult(query, 1)
}
render() {
return (
<div>
<input
type="text"
value={this.state.inputValue}
placeholder="Search movie..."
className={style.field}
onChange={this.handleChange.bind(this)}
/>
</div>
);
}
export default SearchBar;
Component App.js
class App extends React.Component {
render(){
return (
<div>
<BrowserRouter>
<NavBar />
<Routes/>
</BrowserRouter>
</div>
);
}
}
export default App;
Query for search results (Managed with Redux)
export function applyInitialResult(query, page){
return function(dispatch){
getFilmsFromApiWithSearchedText(query, page).then(data => {
if(page === 1){
dispatch({
type:AT_SEARCH_MOVIE.SETRESULT,
query:query,
payload:data,
})
}
})
}
}

Instead of splitting up the routes, you could just use an optional param and handle the query or lack thereof in the component by changing <Route path="/search/:search" component={Search} /> to <Route path="/search/:search?" component={Search} /> and removing <Route exact path='/search' component={() => <Search query={props.text} />} /> entirely.
With that change, you can then get the current query by looking at the value of props.match.params.search in this component. Since you're updating the URL each time the user changes the input, you then don't need to worry about managing it in the component state. The biggest issue with this solution is you'll probably want to delay the search for a little bit after render, otherwise you'll be triggering a call on every keystroke.
EDITED IN RESPONSE TO QUESTION UPDATE
You're right, if applyInitialResult is just an action creator, it won't be async or thenable. You still have options, though.
For example, you could update your action creator so it accepts callbacks to handle the results of the data fetch. I haven't tested this, so treat it as pseudocode, but the idea could be implemented like this:
Action creator
export function applyInitialResult(
query,
page,
// additional params
signal,
onSuccess,
onFailure
// alternatively, you could just use an onFinished callback to handle both success and failure cases
){
return function(dispatch){
getFilmsFromApiWithSearchedText(query, page, signal) // pass signal so you can still abort ongoing fetches if input changes
.then(data => {
onSuccess(data); // pass data back to component here
if(page === 1){
dispatch({
type:AT_SEARCH_MOVIE.SETRESULT,
query:query,
payload:data,
})
}
})
.catch(err => {
onFailure(data); // alert component to errors
dispatch({
type:AT_SEARCH_MOVIE.FETCH_FAILED, // tell store to handle failure
query:query,
payload:data,
err
})
})
}
}
searchMovie Reducer:
// save in store results of search, whether successful or not
export function searchMovieReducer(state = {}, action) => {
switch (action.type){
case AT_SEARCH_MOVIE.SETRESULT:
const {query, payload} = action;
state[query] = payload;
break;
case AT_SEARCH_MOVIE.FETCH_FAILED:
const {query, err} = action;
state[query] = err;
break;
}
}
Then you could still have the results/errors directly available in the component that triggered the fetch action. While you'll still be getting the results through the store, you could use these sort of triggers to let you manage initialChange in the component state to avoid redundant action dispatches or the sort of infinite loops that can pop up in these situations.
In this case, your Searchbar component could look like:
class SearchBar extends React.Component {
constructor(props){
this.controller = new AbortController();
this.signal = this.controller.signal;
this.state = {
fetched: false,
results: props.results // <== probably disposable based on your feedback
}
}
componentDidMount(){
// If search is not undefined, get results
if(this.props.match.params.search){
this.search(this.props.match.params.search);
}
}
componentDidUpdate(prevProps){
// If search is not undefined and different from prev query, search again
if(this.props.match.params.search
&& prevProps.match.params.search !== this.props.match.params.search
){
this.search(this.props.match.params.search);
}
}
setParams({ query }) {
const searchParams = new URLSearchParams();
searchParams.set("query", query || "");
return searchParams.toString();
}
handleChange = (event) => {
const query = event.target.value
const url = this.setParams({ query: query });
this.props.history.replace(`/search/${url}`);
}
search = (query) => {
if(!query) return; // do nothing if empty string passed somehow
// If some search occurred already, let component know there's a new query that hasn't yet been fetched
this.state.fetched === true && this.setState({fetched: false;})
// If some fetch is queued already, cancel it
if(this.willFetch){
clearInterval(this.willFetch)
}
// If currently fetching, cancel call
if(this.fetching){
this.controller.abort();
}
// Finally queue new search
this.willFetch = setTimeout(() => {
this.fetching = this.props.applyInitialResult(
query,
1,
this.signal,
handleSuccess,
handleFailure
)
}, 500 /* wait half second before making async call */);
}
handleSuccess(data){
// do something directly with data
// or update component to reflect async action is over
}
handleFailure(err){
// handle errors
// or trigger fetch again to retry search
}
render() {
return (
<div>
<input
type="text"
defaultValue={this.props.match.params.search} // <== make input uncontrolled
placeholder="Search movie..."
className={style.field}
onChange={this.handleChange.bind(this)}
/>
<div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
// get results from Redux store based on url/route params
results: ownProps.match.params.search
? state.searchMovie[ownProps.match.params.search]
: []
});
const mapDispatchToProps = dispatch => ({
applyInitialResult: // however you're defining it
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearchBar)
EDIT 2
Thanks for the clarification about what you're imagining.
The reason this.props.match.params is always blank is because that's only available to the Search component - the Searchbar is entirely outside the routing setup. It also renders whether or not the current path is /search/:search, which is why withRouter wasn't working.
The other issue is that your Search route is looking for that match param, but you're redirecting to /search?query=foo, not /search/foo, so match params will be empty on Search too.
I also think the way you were managing the initialChange state was what caused your search value to remain unchanged. You handler gets called on every change event for the input, but it shuts itself off after the first keystroke and doesn't turn on again until the input is cleared. See:
if (event.target.value === '') {
this.props.history.goBack()
this.setState({ initialChange: true }) // <== only reset in class
return;
}
...
if(event.target.value.length && this.state.initialChange){
this.setState({
initialChange:false
}, ()=> {
// etc...
})
}
This is what the pattern I was suggesting accomplishes - instead of immediately turning off your handler, set a delay for the dispatch it and keep listening for changes, searching only once user's done typing.
Rather than copying another block of code here, I instead made a working example on codesandbox here addressing these issues. It could still use some optimization, but the concept's there if you want to check out how I handled the Search page, SearchBar, and action creators.
The Searchbar also has a toggle to switch between the two different url formats (query like /search?query=foo vs match.param like /search/foo) so you can see how to reconcile each one into the code.

Related

Make Route for multiple variables using React Router

I'm trying to make multiple pages about some game character using React Router
For multiple pages, I want to load data from firebase and insert the data into pages.
the all character page formats are same and just need to change detailed data
For example,
we have
character 'a' : Age : 12, Gender : Male
character 'b' : Age: 13, Gender : Female
and each character page would need to show the character's data with loaded data
I need to show the data of only one character at one page.
Here is my code about routing
function App() {
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
const onCharChange = (text: string) => {
setCurChar(text);
};
return (
<>
<title>Tekken_info 0.1.0</title>
<GlobalStyle />
<Wrapper>
<PageContent>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/Data" exact={true}>
<Page data={data} />
</Route>
</Switch>
</PageContent>
</Wrapper>
</>
);
}
export default App;
But how I did temporarily was not quite satisfying...
I load character data with useState and load it in to '/Data' page
I don't think this is good way and want to make one route for characters.
For example
when we access to '/a' load data about a only 'a'....
If anything you don't understand about my description and question
let me know
Instead of passing the data as props to Page component, You could move the useEffect into the page itself. so that only curChar is a route path variable and can be accessed inside the page something like:
/Data/curChar
Change the path to
path="/Data/:curChar"
Inside the page
const Page = () => {
let {
curChar
} = useParams();
//move the useEffect here.
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
return <div > Now showing details of {
curChar
} < /div>;
}

React Redux parent and child component sharing state URL keeps reloading old ID

so I have a redux store with has this basic structure:
{
user: {
id: 1,
currentCompanyId: 2,
}
companyDetails: {
id: 2,
name: 'Joes Company'
},
otherCompanies: [2,3,4],
}
I have a parent page which in the header has a dropdown that allows the user to link / switch to another company.
The parent component needs to know which company is selected so it can show the name in the heading.
The child component displays the details of the current company.
There will be various types of companies and the url's / sections will be different for each type. So in the child component I was trying to set the user's company and then load the details of that company.
The child component doesn't need to directly reference the user or current company.
So what I was doing was in the child component I would listen in willmount and willreceiveprops for a change to the url, then fire an action to update the user company. This will then cause the parent to re render as the user object has changed. Which in turn will create a new / remount the child component. So far this seemed logical.
The issue is that when I have selected company 2 and try to switch to company 3, it will set the company to 3, but then reset it back to 2 again.
I am not sure if this is to do with the URL having not updated or something. I have gone around in circles so much now that I am not sure what the workflow should be anymore.
edit
if I comment out this.loadContractorInfo(contractorId); from ContractorHome.js in componentWillMount() it will work correctly (i.e. the URL stays with the number in the link, vs reverting to the old one. I assume this is to do with redux actions are async, and although I am not doing any network calls it is some race condition between getting data for the contractor page and the contextheader wanting to display / update the current company
edit 2
so to confirm. I have loaded the page at the root of the site, all fine. I select a company / contractor from the dropdown. this loads fine. I go to change that selection to a different contractor from the dropdown. The first component to be hit will be the parent (contextheader), the location prop in nextprops will have updated to have the correct ID in the URL. the method execution at this point will NOT update any thing, no actions are fired. It then hits the child (contractorhome) willreceiveprops method, again the URL in location is good as is the match params. I have commented out all code in willrecieveprops so it does not do anything here either. It will then go back to the parent willreceive props and the location will have gone back to the previous ID in the URL.
app.js snippet:
render() {
return (
<div className="app">
<Router>
<div>
<div className="bodyContent">
<ContextHeader />
<Switch>
{/* Public Routes */}
<Route path="/" exact component={HomePage} />
<Route path="/contractor" component={ContractorRoute} />
<Route path="/building" component={BuildingRoute} />
</Switch>
</div>
<Footer />
</div>
</Router>
</div>
);
}
contextheader:
componentWillReceiveProps(nextProps) {
const { setCompany } = this.props;
const currentInfo = this.props.sharedInfo && this.props.sharedInfo.currentCompany;
const newInfo = nextProps.sharedInfo && nextProps.sharedInfo.currentCompany;
if (newInfo && newInfo.id && (!currentInfo || currentInfo.id !== newInfo.id)) {
setCompany(newInfo.id, newInfo.type);
}
}
render() {
const { user, companies, notifications } = this.props;
/* render things here */
}
);
}
}
const mapStateToProps = state => ({
user: state.user,
sharedInfo: state.sharedInfo,
companies: state.companies,
notifications: state.notifications,
});
const mapDispatchToProps = dispatch => ({
setCompany: (companyId, type) => dispatch(setCurrentCompany(companyId, type)),
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ContextHeader));
contractorRoute:
const ContractorRoute = ({ match }) => (
<Switch>
<Route path={`${match.path}/:contractorId`} component={ContractorHome} />
</Switch>
);
contractorHome
componentWillMount() {
const contractorId = parseInt(this.props.match.params.contractorId, 10);
this.setSharedCompany(contractorId);
this.loadContractorInfo(contractorId);
}
componentWillReceiveProps(nextProps) {
const newContractorId = parseInt(nextProps.match.params.contractorId, 10);
if (this.props.match.params.contractorId !== nextProps.match.params.contractorId) {
this.setSharedCompany(newContractorId);
}
}
setSharedCompany(contractorId) {
const { sharedInfo, setCompany } = this.props;
if (typeof contractorId === 'number') {
if (!sharedInfo || !sharedInfo.currentCompany || !sharedInfo.currentCompany.id || sharedInfo.currentCompany.id !== contractorId) {
setCompany(contractorId);
}
}
}
loadContractorInfo(contractorId) {
const { sharedInfo, getContractorInfo, busy } = this.props;
if (!busy && sharedInfo && sharedInfo.currentCompany && sharedInfo.currentCompany.id === contractorId) {
getContractorInfo(contractorId);
}
}
render() { /*render lots of things here*/};
const mapStateToProps = (state) => {
const selector = state.contractor.details;
return {
sharedInfo: state.sharedInfo,
details: selector.info,
error: selector.request != null ? selector.request.error : null,
busy: selector.request && selector.request.busy,
};
};
const mapDispatchToProps = dispatch => ({
getContractorInfo: contractorId => dispatch(getContractor(contractorId)),
setCompany: contractorId => dispatch(setSharedinfoCurrentCompany(contractorId, 'contractor')),
});
export default connect(mapStateToProps, mapDispatchToProps)(ContractorHome);

HOC/Render-Call Back or Library function?

I'm working on a project where a prospect needs to be sent an email about a property they are interested in. There is a top level component that fetches the property information and prospect's contact info from the database and passes to its children. There are two components that share the same process of formatting the information, and then call an email function that sends off an email. A sample of one component looks like this:
import sendEmail from 'actions/sendEmail'
class PropertyDetail extends React.Componet {
state = {
unit: undefined,
prospect: undefined,
};
componentDidMount = () => {
this.setState({
unit: this.props.unit,
prospect: this.props.prospect,
});
};
sendEmail = ({ id, address, prospect }) => {
// quite a bit more gets formatted and packaged up into this payload
const payload = {
id,
address,
prospectEmail: prospect.email,
};
emailFunction(payload);
};
handleEmail = () => {
sendEmail(this.state);
};
render() {
return (
<div>
<h1>{this.state.unit.address}</h1>
<p>Send prospect an email about this property</p>
<button onClick={this.handleEmail}>Send Email</button>
</div>
);
}
}
and the other component looks like this
class UpdateShowing extends React.Component {
state = {
unit: undefined,
prospect: undefined,
showingTime: undefined,
};
componentDidMount = () => {
this.setState({
unit: this.props.unit,
propsect: this.props.prospect,
showingTime: this.props.showingTime,
});
};
sendEmail = ({ id, address, prospectEmail }) => {
// quite a bit more gets formatted and packaged up into this payload
const payload = {
id,
address,
prospectEmail,
};
emailFunction(payload);
};
handleUpdate = newTime => {
// get the new date for the showing ...
this.setState({
showingTime: newTime,
});
// call a function to update the new showing in the DB
updateShowingInDB(newTime);
sendEmail(this.state);
};
render() {
return (
<div>
<p>Modify the showing time</p>
<DatePickerComponent />
<button onClick={this.handleUpdate}>Update Showing</button>
</div>
);
}
}
So I see some shared functionality that I'd love to not have to repeat in each component. I'm still learning (working my first job), and why not use this as an opportunity to grow my skills? So I want to get better at the HOC/Render props pattern, but I'm not sure if this is the place to use one.
Should I create a component with a render prop (I'd rather use this pattern instead of a HOC)? I'm not even sure what that would look like, I've read the blogs and watched the talks, ala
<MouseMove render={(x, y) => <SomeComponent x={x} y={y} />} />
But would this pattern be applicable to my case, or would I be better off defining some lib function that handles formatting that payload for the email and then importing that function into the various components that need it?
Thanks!
I think a provider or a component using render props with branching is a better fit for you here
see this doc: https://lucasmreis.github.io/blog/simple-react-patterns/#render-props

React/Redux controlled input with validation

Lets imagine we want an input for a "product" (stored in redux) price value.
I'm struggle to come up with the best way to handle input constraints. For simplicity, lets just focus on the constraint that product.price cannot be empty.
It seems like the 2 options are:
1: Controlled
Implementation: The input value is bound to product.price. On change dispatches the changePrice() action.
The main issue here is that if we want to prevent an empty price from entering the product store, we essentially block the user from clearing the input field. This isn't ideal as it makes it very hard to change the first digit of the number (you have to select it and replace it)!
2: Using defaultValue
Implementation: We set the price initially using input defaultValue, that allows us to control when we want to actually dispatch changePrice() actions and we can do validation handling in the onChange handler.
This works well, unless the product.price is ever updated from somewhere other than the input change event (for example, an applyDiscount action). Since defaultValue doesn't cause rerenders, the product.price and the input are now out of sync!
So what am I missing?
There must be a simple & elegant solution to this problem but I just can't seem to find it!
What I have done in the past is to use redux-thunk and joi to solve input constraints/validation using controlled inputs.
In general I like to have one update action that will handle all the field updating. So for example if you have two inputs for a form, it would looks something like this:
render() {
const { product, updateProduct } = this.props;
return (
<div>
<input
value={product.name}
onChange={() => updateProduct({...product, name: e.target.value})}
/>
<input
value={product.price}
onChange={() => updateProduct({...product, price: e.target.value})}
/>
</div>
)
}
Having one function/action here simplifies my forms a great deal. The updateProject action would then be a thunk action that handles side effects. Here is our Joi Schema(based off your one requirement) and updateProduct Action mentioned above. As a side note, I also tend to just let the user make the mistake. So if they don't enter anything for price I would just make the submit button inactive or something, but still store away null/empty string in the redux store.
const projectSchema = Joi.object().keys({
name: Joi.number().string(),
price: Joi.integer().required(), // price is a required integer. so null, "", and undefined would throw an error.
});
const updateProduct = (product) => {
return (dispatch, getState) {
Joi.validate(product, productSchema, {}, (err, product) => {
if (err) {
// flip/dispatch some view state related flag and pass error message to view and disable form submission;
}
});
dispatch(update(product)); // go ahead and let the user make the mistake, but disable submission
}
}
I stopped using uncontrolled inputs, simply because I like to capture the entire state of an application. I have very little local component state in my projects. Keep in mind this is sudo code and probably won't work if directly copy pasted. Hope it helps.
So I think I've figure out a decent solution. Basically I needed to:
Create separate component that can control the input with local state.
Pass an onChange handler into the props that I can use to dispatch my changePrice action conditionally
Use componentWillReceiveProps to keep the local value state in sync with the redux store
Code (simplified and in typescript):
interface INumberInputProps {
value: number;
onChange: (val: number) => void;
}
interface INumberInputState {
value: number;
}
export class NumberInput extends React.Component<INumberInputProps, INumberInputState> {
constructor(props) {
super(props);
this.state = {value: props.value};
}
public handleChange = (value: number) => {
this.setState({value});
this.props.onChange(value);
}
//keeps local state in sync with redux store
public componentWillReceiveProps(props: INumberInputProps){
if (props.value !== this.state.value) {
this.setState({value: props.value});
}
}
public render() {
return <input value={this.state.value} onChange={this.handleChange} />
}
}
In my Product Component:
...
//conditionally dispatch action if meets valadations
public handlePriceChange = (price: number) => {
if (price < this.props.product.standardPrice &&
price > this.props.product.preferredPrice &&
!isNaN(price) &&
lineItem.price !== price){
this.props.dispatch(updatePrice(this.props.product, price));
}
}
public render() {
return <NumberInput value={this.props.product.price} onChange={this.handlePriceChange} />
}
...
What i would do in this case is to validate the input onBlur instead of onChange.
For example consider these validations in the flowing snippet:
The input can't be empty.
The input should not contain "foo".
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
myVal: '',
error: ''
}
}
setError = error => {
this.setState({ error });
}
onChange = ({ target: { value } }) => {
this.setState({ myVal: value })
}
validateInput = ({ target: { value } }) => {
let nextError = '';
if (!value.trim() || value.length < 1) {
nextError = ("Input cannot be empty!")
} else if (~value.indexOf("foo")) {
nextError = ('foo is not alowed!');
}
this.setError(nextError);
}
render() {
const { myVal, error } = this.state;
return (
<div>
<input value={myVal} onChange={this.onChange} onBlur={this.validateInput} />
{error && <div>{error}</div>}
</div>
);
}
}
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>
<div id="root"></div>
Edit
As a followup to your comments.
To make this solution more generic, i would pass the component a predicate function as a prop, only when the function will return a valid result i would call the onChange that passed from the parent or whatever method you pass that updating the store.
This way you can reuse this pattern in other components and places on your app (or even other projects).

Where to put chained actions in React/Redux

Fairly simple use case: I have actions/events that will cause an ajax request to be executed and then update a list.
The problem is I'm not sure how (specifically, where to kick off a request for a new list when the page is changed.
redux store
const defaultStore = {
list: [],
page: 1
};
Wrapper Component
const wrapper = props => (
<div>
<List {...props}> {/* This list should have page controls */}
<PageControls {...props} />
</List>
<List /> {/* This list should not have page controls */}
</div>
);
List component
const List = props => (
<div>
{props.children} {/* render page controls if present */}
{props.items.map((item, k) => <div key={k}>item</div>
</div>
);
Pager Controls component
const PageControls = props => (
<div>
<span onClick={props.changePage(-1)}>Backward</span>
<span onClick={props.changePage(1)}>Forward</span>
</div>
);
actionCreator
function changePage(delta) {
return {
type: 'CHANGE_PAGE',
delta
};
}
// utilizes react-thunk middleware
function getList(page = 1) {
return dispatch =>
axios.get(`/path/to/api?page=${page}`)
.then(res => dispatch(updateList(res.data));
}
function updateList(newList) {
return {
type: 'UPDATE_LIST',
newList
};
}
reducer
function reducer(state = {}, action) {
switch(action.type) {
case 'CHANGE_PAGE':
return {...state, page: state.page + action.delta};
case 'UPDATE_LIST':
return {...state, list: action.newList};
default:
return state;
}
}
At this point I could do a couple of things -- I could make every actionCreator that should trigger a list update dispatch that action:
function changePage(delta) {
return dispatch => {
dispatch({
type: 'CHANGE_PAGE',
delta
});
return dispatch(getList(store.getState() + delta));
}
}
But this seems messy. Now not only do I have to get my store but I also have to turn every actionCreator that affects the list into a thunk.
The only other thing I can think of is to have my <List> component use store.subscribe somewhere to watch for changes to page and then kick off another getList action, but this also seems like I'm moving the understanding of what does and does not trigger state changes out of Redux and into my React components.
Any ideas?
Well, maybe you should change your approach. I don't see a reason to make two actions for changing page and retrieving the list. You can just dispatch getPage() action on button click, passing next page number. This should retrieve list of items and refresh your page.
In your store you should keep track on current page, so each time page refreshes the value of getPage() argument will also update.
For example (assuming that current page is not retrieved from API):
function getPage(page = 1) {
return dispatch =>
axios.get(`/path/to/api?page=${page}`)
.then(res => dispatch(updatePage(res.data, page));
}
function updatePage(newList, currentPage) {
return {
type: 'UPDATE_PAGE',
newList,
currentPage,
};
}
and connect required components to the store, in your case it would be List and PageControls components.
const PageControls = props => (
<div>
<span onClick={props.getPage(props.currentPage - 1)}>Backward</span>
<span onClick={props.getPage(props.currentPage + 1)}>Forward</span>
</div>
);
This will allow you to maintain simple and clean code. Also you can trigger it from multiple, not related components.

Resources