3

I'm trying to build an app with React and Redux (DVA). It's using Ant.Design as the main framework. I'm trying to change the URL when the user clicks on a button, and obviously 'bind' that url change to an action, so that if the user goes directly to that URL, he gets what he wants.

At the moment here's what I have, in a function in my component.

const { dispatch, match } = this.props;
dispatch(routerRedux.push('/f/' + record.id));

This is the only thing that I was able to produce. It correctly changes the url, but doesn't bind the url with a specific behaviour, making it completely useless.

How do I link the URL with an action?

2 Answers 2

2

If you wish to trigger an action based on a URL, you'll need to use react-router to route a component that then performs the desired action. In such a case it is also a good idea to then visit a different URL, erasing the action-URL from the browser's history.

A typical router definition might look something like this (taken from react-router-redux's docs):

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <div>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
        <Route path="/success" component={Success}/>
      </div>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
)

So you wish to add a path /f/<record-id>. You can do this by adding a line like this:

<Route path="/f/:recordId" component={MyActionComponent}/>

Now you need to define a component, MyActionComponent, that will perform your action.

import { connect } from 'react-redux';
import { replace } from 'react-router-redux';

const mapDispatchToProps = (dispatch: Dispatch) => ({
  visitNextLocation: () => dispatch(replace('/success')),
  myAction: (recordId) => dispatch(myAction(recordId)),
});

const withDispatch = connect(null, mapDispatchToProps);

class MyActionComponent extends Component {
  props: {
    match: {
      params: {
        recordId: string,
      }
    },
    redirectToLogin: () => void,
    myAction: string => void,
  };

  componentWillMount() {
    const recordId = this.props.match.params.recordId;
    if (recordId) {
      this.props.myAction(token);
      this.props.visitNextLocation();
    }
  }

  render() {
    return null;
  }
}

Note the use of replace instead of push. This means, when a user visits this URL their action will get performed and they'll end up on /success. But if they click the Back button, they won't then revisit this URL and run the action again.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for you help, but unfortunately my app is designed in a way that each view is divided in 2 parts. The left panel always stays the same and contains a bunch of links, each link changing what the user sees in the right panel. So basically, I have a component called "users" which contains 2 components: LeftPanel and RightPanel. Therefore I have <Route path="/users" component={users}/> and it indeed works fine when I try localhost:8000/users, but then inside the Users component, the component LeftPanel should modify the URL too
Could you share an example, maybe using codepen? What's you've said about a component modifying the URL is confusing for me. Are you saying that an action causes a push dispatch or that the component is modifying the URL some other way? Or are you talking about routing?
Can you please take a look at my answer to this question?
0

I can't put the code on Codepen for privacy reasons. But here's an extract:

router.js

...
},
'/users': {
  component: dynamicWrapper(app, ['rule'], () => import('../routes/users')),
},
'/f/:userID': {
  component: dynamicWrapper(app, ['rule'], () => import('../routes/users')),
},
...

users.js (the main component that contains LeftPanel and RightPanel)

import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, List, Divider, Badge, Select, Radio, Input, Popover, Button, Table, Spin } from 'antd';
import RightPanel from './RightPanel';
import LeftPanel from './LeftPanel';
import { routerRedux, Route, Switch } from 'dva/router';
import 'font-awesome/css/font-awesome.min.css';
import FadeIn from 'react-fade-in';

@connect(({ rule, loading }) => ({rule, loading: loading.models.rule }))
export default class Users extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          selected_user: [],
          defaultView: true,
          isLoadingNow: false,
          selectedRowKeys: [],
          makeEmpty: false,
          searchRes: []
      }
    }

        selectRow = (record) => {       
            const { dispatch, match } = this.props;
            dispatch(routerRedux.replace({ pathname: '/f/' + record.id }));

            this.setState({isLoadingNow: true, selectedRowKeys: record.key})
            setTimeout(() => {
              this.setState({
                    isLoadingNow: false,
                    defaultView: false,
                    selected_user: record
                })
            }, 75)
        }

        componentDidMount() { 
            const { dispatch, match } = this.props;
            dispatch({
                type: 'rule/fetch'
            });
            if (match.params.userID == undefined) {
                // do nothing
            } else if (match.params.userID) {
                var result = this.props.rule.data.list.filter(function( obj ) {
                  return obj.id == match.params.userID;
                });
                this.selectRow.bind(this, result[0])
            }
        }

        render() {

            const { selectedRowKeys } = this.state;
            const rowSelection = {
                selectedRowKeys,
                type:"radio"
            };

            const { rule: { data }, loading } = this.props;

            return (<div>

                            <LeftPanel
                                rowSelection={rowSelection}
                                dataSource={this.state.makeEmpty ? this.state.searchRes : this.props.rule.data.list} 
                                selectRow={this.selectRow} 
                                loadingStatus={loading}
                            />
                          <RightPanel 
                                selected_user={this.state.selected_user}
                                is_default={this.state.defaultView}
                                loading={this.state.isLoadingNow}
                          />
                        </div>);


}
}

leftPanel.js (responsible for displaying the list of links, on which the user will click on one, which will do 2 things:
- change the url accordingly
- display specific data on RightPanel.js)

import React from 'react';
import { Table, Card } from 'antd';

import styles from './index.less';
// import 'font-awesome/css/font-awesome.min.css';

import { Row, Col, List, Divider, Badge, Select, Radio, Input, Popover, Button } from 'antd';

var moment = require('moment');

class LeftPanel extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            selected_row_index: undefined
        }
    }   
    handleChangeStyleOnSelectRow(index) {
        this.setState({
            selected_row_index: index
        }, console.log(this.state.selected_row_index))
    }

    prettifyForTable(raw_data) {
      var prettyRows = [];
      raw_data.map((item,index) =>
        prettyRows.push(
          <div style={{"width": "100%"}}> 
            <Row style={{  "align-items": "center"}} type="flex" justify="space-between">
                <Col span={10}>
                    <div style={{"font-size": "15px", "text-align": "center"}}>
                        {item.user_name} <i style={{"color": "rgba(0, 0, 0, 0.25)", "margin": "0 10px", "transform": "rotate(45deg)"}} className="fa fa-plane"> </i> {item.user_age}
                        <div style={{"font-size": "12px", "color": "grey"}}> {moment(item.user_color).format('HH:MM')} · {moment(item.user_order).format('HH:MM')}  </div>
                    </div>
                </Col>
                <Col span={3}>
                    <div style={{"text-align": "right", "text-align": "center"}}>
                      {item.user_family}
                    </div>
                </Col>
                <Col span={6}>
                    <div style={{"text-align": "right", "text-align": "center"}}>
                      {moment(item.user_height).format('MMMM D')}
                    </div>
                </Col>
                <Col span={3}>
                    <div style={{"text-align": "center"}}>
                        {(item.status == "in_progress") ? <div> <Badge style={{"padding-right": "25px"}} status="processing"/></div> : <div style={{"text-align": "center"}}> <Badge style={{"padding-right": "25px"}} status="default"/></div>}
                    </div>
                </Col>
            </Row>
          </div>
        )
      );
      return prettyRows;
    }


  render() {
    const stylesSelectedRow = { "background": "rgba(155,155,155,0.05)", "box-shadow": "0 0 5px 0 #4A90E2", "transform": "scale(1.01)"};
    const { dataSource } = this.props;
        return(
         <div>
            {dataSource &&
          <Card bordered={false} loading={this.props.loadingStatus} className={styles.userRows} bodyStyle={{"padding": "0 15px"}}>
                <List
                  size="small"
                  bordered={false}
                  dataSource={this.prettifyForTable(dataSource)}
                  renderItem={(item, index) => (<List.Item onClick={() => {this.state.selected_row_index == index ? null : this.props.selectRow(this.props.dataSource[index]); this.handleChangeStyleOnSelectRow(index)}} style={this.state.selected_row_index == index ? stylesSelectedRow : null} className={styles.userRows}>{item}</List.Item>)}
                />
          </Card>
            }
        </div>
    )
  }
}

export default LeftPanel;

and finally RightPanel.js, that is reponsible for listening to the URL or a click on LeftPanel, and display data accordingly.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import infopng from '../../../assets/info.svg';

import TabInformations from './TabInformations.js';
import TabFamily from './TabFamily.js';
import TabProblems from './TabProblems.js';
import { Tabs, Button, Spin, Icon, Table, Pagination, Card, Col, Row, Spinner, Badge } from 'antd';
const TabPane = Tabs.TabPane;
import 'font-awesome/css/font-awesome.min.css';
import WindowSizeListener from 'react-window-size-listener'
import FadeIn from 'react-fade-in';

export default class RightPanel extends Component {

  render() {

    if (this.props.loading) {
        return(
            <div> 
          <Spin 
            indicator={<Icon type="loading" style={{ fontSize: 24 }} spin />}
                    src="http://web.gndu.ac.in/DepartmentProfile/images/spinner.gif"
                />
            </div>
        );
    } else if (this.props.is_default) {
      return(
          <div style={{"margin-top": "64px", "margin-right": "10px", "text-align": "center", "height": "90vh", "display": "flex", "align-items": "center", "justify-content": "center"}}> 
            <div>
                <img src={infopng} style={{"height": "155px"}} />
                <p style={{"color": "#8e8e8e"}}> select a user on the <br/> left-hand side... </p>
               </div>
          </div>
        );
    } else {
    return (

      <FadeIn>

      <Card bodyStyle={{"padding": "0"}} style={{"background-color": "white", "height":"90vh", "padding": "20px", "box-shadow": "rgba(0, 21, 41, 0.1) 0px 0px 6px", "opacity": "1", "transition": "background 0.6s", "border-radius": "2px", "margin": "10px 10px 0 0px", "margin-top": "64px"}}> 
            <Tabs defaultActiveKey="1" style={{"text-align": "center", "padding": "0 15px"}}>
        <TabPane tab="General" key="1">
          <TabInformations
                    selected_user={this.props.selected_user}
                />
            </TabPane>
            <TabPane tab="Servicing" key="2">
          <TabFamily 
            selected_user={this.props.selected_user} 
          />
          </TabPane>
          <TabPane tab={<div>Defect(s)<Badge showZero count={0} style={{ backgroundColor: '#fff', color: '#999', boxShadow: '0 0 0 1px #d9d9d9 inset', "margin-left": "10px" }} /></div>} key="3">
          <TabProblems />
        </TabPane>
        </Tabs>
    </Card>
    </FadeIn>
    );
  }
}
}

This code does the initial job pretty well: when the user clicks on a link on leftPanel.js, leftPanel.js calls the method selectRow in users.js, which in turn selects a row and display it on RightPanel.js.

My question: how to add to this the fact that it changes the URL whenever the user clicks? And obviously, if the user clicks on "go back" in Chrome, the data in RightPanel.js has to change accordingly.. ?

2 Comments

This is quite different to what I thought you were asking! It looks to me like you're attempting to visit a different URL, but instead of actually visiting it you're managing what you're looking at via state in users.js. I think it would be easier if you drove the selected_user by this.props and in selectRow you just did dispatch(router.push(...)). .push will add a URL to your history so that when you click back it remembers where you were before.
I haven't used dva before, but you might want to move LeftPanel to be a sibling of the other routes since it's shared across routes. You can see an example of nav being shared here: tylermcginnis.com/react-router-nested-routes (I think that only works in react-router v4 though)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.