React / Redux - ส่งการดำเนินการในการโหลด / เริ่มต้นของแอป


88

ฉันมีการตรวจสอบความถูกต้องโทเค็นจากเซิร์ฟเวอร์ดังนั้นเมื่อแอป Redux ของฉันถูกโหลดในตอนแรกฉันต้องร้องขอไปยังเซิร์ฟเวอร์นี้เพื่อตรวจสอบว่าผู้ใช้ได้รับการรับรองความถูกต้องหรือไม่และถ้าใช่ฉันควรได้รับโทเค็น

ฉันพบว่าไม่แนะนำให้ใช้การดำเนินการ INIT หลักของ Redux ดังนั้นฉันจะส่งการดำเนินการก่อนที่แอปจะแสดงผลได้อย่างไร

คำตอบ:


78

คุณสามารถส่งการดำเนินการในcomponentDidMountวิธีการรูทและในrenderวิธีการคุณสามารถตรวจสอบสถานะการรับรองความถูกต้องได้

สิ่งนี้:

class App extends Component {
  componentDidMount() {
    this.props.getAuth()
  }

  render() {
    return this.props.isReady
      ? <div> ready </div>
      : <div>not ready</div>
  }
}

const mapStateToProps = (state) => ({
  isReady: state.isReady,
})

const mapDispatchToProps = {
  getAuth,
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

1
สำหรับฉันcomponentWillMount()ทำสิ่งนั้น ฉันกำหนดฟังก์ชั่นที่เรียบง่ายเรียกการกระทำที่เกี่ยวข้องทั้งหมดในการจัดส่งmapDispatchToProps()ของ App.js componentWillMount()และเรียกมันใน
Froxx

นี่เป็นสิ่งที่ดี แต่การใช้ mapDispatchToProps ดูเหมือนจะอธิบายได้มากกว่า เหตุผลของคุณอยู่เบื้องหลังการใช้ mapStateToProps แทนอะไร
tcelferact

@ adc17 Oooops :) ขอบคุณสำหรับความคิดเห็น ฉันเปลี่ยนคำตอบแล้ว!
Serhii Baraniuk

1
@ adc17 อ้างจากdoc :[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): If an object is passed, each function inside it is assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly, will be merged into the component’s props.
Serhii Baraniuk

2
ฉันได้รับข้อผิดพลาดนี้เมื่อพยายามใช้โซลูชันนี้Uncaught Error: Could not find "store" in either the context or props of "Connect(App)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(App)".
markhops

35

ฉันไม่พอใจกับวิธีแก้ปัญหาใด ๆ ที่ถูกหยิบยกมาเพื่อสิ่งนี้และจากนั้นก็เกิดขึ้นกับฉันจนฉันคิดเกี่ยวกับชั้นเรียนที่จำเป็นต้องแสดงผล จะเกิดอะไรขึ้นถ้าฉันเพิ่งสร้างคลาสสำหรับการเริ่มต้นแล้วผลักดันสิ่งต่าง ๆ ลงในcomponentDidMountเมธอดแล้วเพียงแค่renderแสดงหน้าจอโหลด

<Provider store={store}>
  <Startup>
    <Router>
      <Switch>
        <Route exact path='/' component={Homepage} />
      </Switch>
    </Router>
  </Startup>
</Provider>

แล้วมีสิ่งนี้:

class Startup extends Component {
  static propTypes = {
    connection: PropTypes.object
  }
  componentDidMount() {
    this.props.actions.initialiseConnection();
  }
  render() {
    return this.props.connection
      ? this.props.children
      : (<p>Loading...</p>);
  }
}

function mapStateToProps(state) {
  return {
    connection: state.connection
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(Actions, dispatch)
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Startup);

จากนั้นเขียนการกระทำที่ซ้ำซ้อนเพื่อ async เริ่มต้นแอปของคุณ ใช้งานได้จริง


ตอนนี้เป็นทางออกที่ฉันกำลังมองหา! ฉันเชื่อว่าข้อมูลเชิงลึกของคุณตรงนี้ถูกต้อง ขอบคุณ.
YanivGK

30

คำตอบทั้งหมดที่นี่ดูเหมือนจะเป็นรูปแบบต่างๆในการสร้างส่วนประกอบรูทและเริ่มทำงานใน componentDidMount สิ่งหนึ่งที่ฉันชอบมากที่สุดเกี่ยวกับ Redux คือการแยกข้อมูลที่ดึงมาจากวงจรชีวิตของส่วนประกอบ ฉันไม่เห็นเหตุผลว่าทำไมมันควรจะแตกต่างกันในกรณีนี้

หากคุณกำลังนำเข้าร้านค้าของคุณไปยังindex.jsไฟล์รูทคุณสามารถส่งผู้สร้างการกระทำของคุณ (เรียกว่าinitScript()) ในไฟล์นั้นและจะเริ่มทำงานก่อนที่จะโหลดอะไร

ตัวอย่างเช่น:

//index.js

store.dispatch(initScript());

ReactDOM.render(
  <Provider store={store}>
    <Routes />
  </Provider>,
  document.getElementById('root')
);

1
ฉันเป็นมือใหม่ที่มีปฏิกิริยาตอบสนอง แต่จากการอ่านเอกสารเบื้องต้นเกี่ยวกับแนวคิด react และ redux ฉันเชื่อว่านี่เป็นวิธีที่เหมาะสมที่สุด มีข้อได้เปรียบในการสร้างการเริ่มต้นเหล่านี้ในcomponentDidMountเหตุการณ์หรือไม่
kuzditomi

1
มันขึ้นอยู่กับสถานการณ์จริงๆ ดังนั้นcomponentDidMountไฟจะเริ่มขึ้นก่อนที่ส่วนประกอบเฉพาะจะติดตั้ง การเริ่มทำงานstore.dispatch()ก่อนที่ ReactDOM.render () `จะเริ่มทำงานก่อนที่แอปจะเชื่อมต่อ มันเหมือนกับcomponentWillMountแอปทั้งหมด ในฐานะมือใหม่ฉันคิดว่าควรยึดติดกับการใช้วิธีวงจรชีวิตของส่วนประกอบจะดีกว่าเพราะมันช่วยให้ตรรกะควบคู่ไปกับที่ที่มีการใช้งาน เมื่อแอปมีความซับซ้อนมากขึ้นสิ่งนี้ก็ยากที่จะทำต่อไป คำแนะนำของฉันคือทำให้มันเรียบง่ายตราบเท่าที่คุณสามารถทำได้
Josh Pittman

1
เมื่อเร็ว ๆ นี้ฉันต้องใช้แนวทางข้างต้น ฉันมีปุ่มเข้าสู่ระบบของ Google และฉันจำเป็นต้องเริ่มการทำงานของสคริปต์เพื่อให้ทำงานได้ก่อนที่แอปจะโหลดขึ้นมา หากฉันรอให้แอปโหลดแล้วทำการโทรมันจะต้องใช้เวลานานกว่าจะได้รับคำตอบและฟังก์ชันการทำงานในแอปล่าช้า หากการทำสิ่งต่างๆในวงจรชีวิตใช้ได้ผลกับกรณีการใช้งานของคุณให้ยึดตามวงจรชีวิต มันง่ายกว่าที่จะคิด วิธีที่ดีในการตัดสินสิ่งนี้คือการนึกภาพว่าตัวเองกำลังมองไปที่รหัส 6 เดือนนับจากนี้ วิธีใดจะง่ายกว่าสำหรับคุณที่จะเข้าใจโดยสังหรณ์ใจ เลือกแนวทางนั้น
Josh Pittman

1
นอกจากนี้คุณไม่จำเป็นต้องสมัครรับการอัปเดตบน Redux เพียงแค่ต้องจัดส่ง นั่นคือจุดรวมของแนวทางนี้ฉันใช้ประโยชน์จากข้อเท็จจริงที่ว่า redux แยกการทำสิ่งนั้น (การดึงข้อมูลการเริ่มการทำงาน ฯลฯ ) และใช้ผลลัพธ์ (การแสดงผลการตอบสนอง ฯลฯ )
Josh Pittman

2
ฉันบอกว่าใช่สำหรับประเด็นของคุณเกี่ยวกับการจัดส่ง Redux ไม่ได้บอกว่าเราต้องส่งการกระทำจากภายในองค์ประกอบปฏิกิริยา Redux เป็นอิสระจากการตอบสนอง
Tuananhcwrs

18

หากคุณใช้ React Hooks วิธีแก้ปัญหาแบบบรรทัดเดียวคือ

useEffect(() => store.dispatch(handleAppInit()), []);

อาร์เรย์ว่างช่วยให้แน่ใจว่ามีการเรียกใช้เพียงครั้งเดียวในการแสดงผลครั้งแรก

ตัวอย่างเต็ม:

import React, { useEffect } from 'react';
import { Provider } from 'react-redux';

import AppInitActions from './store/actions/appInit';
import store from './store';

export default function App() {
  useEffect(() => store.dispatch(AppInitActions.handleAppInit()), []);
  return (
    <Provider store={store}>
      <div>
        Hello World
      </div>
    </Provider>
  );
}

11

อัปเดต 2020 : นอกเหนือจากโซลูชันอื่น ๆ ฉันใช้มิดเดิลแวร์ Redux เพื่อตรวจสอบแต่ละคำขอสำหรับความพยายามในการเข้าสู่ระบบที่ล้มเหลว:

export default () => next => action => {
  const result = next(action);
  const { type, payload } = result;

  if (type.endsWith('Failure')) {
    if (payload.status === 401) {
      removeToken();

      window.location.replace('/login');
    }
  }

  return result;
};

อัปเดต 2018 : คำตอบนี้สำหรับReact Router 3

ฉันแก้ไขปัญหานี้โดยใช้ react-router onEnter props นี่คือลักษณะของโค้ด:

// this function is called only once, before application initially starts to render react-route and any of its related DOM elements
// it can be used to add init config settings to the application
function onAppInit(dispatch) {
  return (nextState, replace, callback) => {
    dispatch(performTokenRequest())
      .then(() => {
        // callback is like a "next" function, app initialization is stopped until it is called.
        callback();
      });
  };
}

const App = () => (
  <Provider store={store}>
    <IntlProvider locale={language} messages={messages}>
      <div>
        <Router history={history}>
          <Route path="/" component={MainLayout} onEnter={onAppInit(store.dispatch)}>
            <IndexRoute component={HomePage} />
            <Route path="about" component={AboutPage} />
          </Route>
        </Router>
      </div>
    </IntlProvider>
  </Provider>
);

11
เพียงเพื่อให้ชัดเจน react-router 4 ไม่รองรับ onEnter
Rob L

IntlProvider ควรให้คำแนะนำวิธีแก้ปัญหาที่ดีกว่า .. ดูคำตอบของฉันด้านล่าง
Chris Kemp

นี้ใช้ react-router v3 เก่าดูคำตอบของฉัน
stackdave

3

ด้วยมิดเดิลแวร์Redx-sagaคุณสามารถทำได้อย่างดี

เพียงกำหนดเทพนิยายที่ไม่ได้เฝ้าดูการกระทำที่ส่งมา (เช่นมีtakeหรือtakeLatest) ก่อนที่จะถูกเรียกใช้ เมื่อforked จาก root saga เช่นนั้นมันจะทำงานเพียงครั้งเดียวเมื่อเริ่มต้นแอพ

ต่อไปนี้เป็นตัวอย่างที่ไม่สมบูรณ์ซึ่งต้องใช้ความรู้เล็กน้อยเกี่ยวกับredux-sagaแพ็คเกจ แต่ต้องอธิบายประเด็น:

sagas / launchSaga.js

import { call, put } from 'redux-saga/effects';

import { launchStart, launchComplete } from '../actions/launch';
import { authenticationSuccess } from '../actions/authentication';
import { getAuthData } from '../utils/authentication';
// ... imports of other actions/functions etc..

/**
 * Place for initial configurations to run once when the app starts.
 */
const launchSaga = function* launchSaga() {
  yield put(launchStart());

  // Your authentication handling can go here.
  const authData = yield call(getAuthData, { params: ... });
  // ... some more authentication logic
  yield put(authenticationSuccess(authData));  // dispatch an action to notify the redux store of your authentication result

  yield put(launchComplete());
};

export default [launchSaga];

โค้ดด้านบนส่งการดำเนินการa launchStartและlaunchCompleteredux ที่คุณควรสร้าง เป็นแนวทางปฏิบัติที่ดีในการสร้างการกระทำดังกล่าวเนื่องจากมีประโยชน์ในการแจ้งให้รัฐทำสิ่งอื่น ๆ เมื่อใดก็ตามที่การเปิดตัวเริ่มต้นหรือเสร็จสิ้น

เทพนิยายรากของคุณควรแยกlaunchSagaเทพนิยายนี้:

sagas / index.js

import { fork, all } from 'redux-saga/effects';
import launchSaga from './launchSaga';
// ... other saga imports

// Single entry point to start all sagas at once
const root = function* rootSaga() {
  yield all([
    fork( ... )
    // ... other sagas
    fork(launchSaga)
  ]);
};

export default root;

โปรดอ่านเอกสารที่ดีจริงๆของ redux-sagaสำหรับข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้


หน้าจะไม่โหลดจนกว่าการดำเนินการนี้จะเสร็จสมบูรณ์ถูกต้อง?
Markov

2

นี่คือคำตอบโดยใช้ล่าสุดใน React (16.8), Hooks:

import { appPreInit } from '../store/actions';
// app preInit is an action: const appPreInit = () => ({ type: APP_PRE_INIT })
import { useDispatch } from 'react-redux';
export default App() {
    const dispatch = useDispatch();
    // only change the dispatch effect when dispatch has changed, which should be never
    useEffect(() => dispatch(appPreInit()), [ dispatch ]);
    return (<div>---your app here---</div>);
}

แอปต้องอยู่ภายใต้ผู้ให้บริการ เพื่อให้ TypeScript มีความสุขฉันต้องปิดโฆษณาเพิ่มเติมในการจัดส่ง: useEffect (() => {dispatch (AppInit ())}, [])
PEZO

0

ฉันใช้ redux-thunk เพื่อดึงข้อมูลบัญชีภายใต้ผู้ใช้จากจุดสิ้นสุดของ API ในการเริ่มต้นแอปและเป็นแบบ async ดังนั้นข้อมูลจึงเข้ามาหลังจากแอปของฉันแสดงผลและโซลูชันส่วนใหญ่ข้างต้นไม่ได้ทำสิ่งมหัศจรรย์สำหรับฉันและบางส่วนก็เป็น คิดค่าเสื่อมราคา ดังนั้นฉันจึงมองไปที่ componentDidUpdate () โดยพื้นฐานแล้วใน APP init ฉันต้องมีรายการบัญชีจาก API และบัญชีร้านค้า redux ของฉันจะเป็นโมฆะหรือ [] ใช้กับสิ่งนี้หลังจากนี้

class SwitchAccount extends Component {

    constructor(props) {
        super(props);

        this.Format_Account_List = this.Format_Account_List.bind(this); //function to format list for html form drop down

        //Local state
        this.state = {
                formattedUserAccounts : [],  //Accounts list with html formatting for drop down
                selectedUserAccount: [] //selected account by user

        }

    }



    //Check if accounts has been updated by redux thunk and update state
    componentDidUpdate(prevProps) {

        if (prevProps.accounts !== this.props.accounts) {
            this.Format_Account_List(this.props.accounts);
        }
     }


     //take the JSON data and work with it :-)   
     Format_Account_List(json_data){

        let a_users_list = []; //create user array
        for(let i = 0; i < json_data.length; i++) {

            let data = JSON.parse(json_data[i]);
            let s_username = <option key={i} value={data.s_username}>{data.s_username}</option>;
            a_users_list.push(s_username); //object
        }

        this.setState({formattedUserAccounts: a_users_list}); //state for drop down list (html formatted)

    }

     changeAccount() {

         //do some account change checks here
      }

      render() {


        return (
             <Form >
                <Form.Group >
                    <Form.Control onChange={e => this.setState( {selectedUserAccount : e.target.value})} as="select">
                        {this.state.formattedUserAccounts}
                    </Form.Control>
                </Form.Group>
                <Button variant="info" size="lg" onClick={this.changeAccount} block>Select</Button>
            </Form>
          );


         }    
 }

 const mapStateToProps = state => ({
      accounts: state.accountSelection.accounts, //accounts from redux store
 });


  export default connect(mapStateToProps)(SwitchAccount);

0

หากคุณใช้ React Hooks คุณสามารถส่งการดำเนินการโดยใช้ React.useEffect

React.useEffect(props.dispatchOnAuthListener, []);

ฉันใช้รูปแบบนี้ในการลงทะเบียนonAuthStateChangedผู้ฟัง

function App(props) {
  const [user, setUser] = React.useState(props.authUser);
  React.useEffect(() => setUser(props.authUser), [props.authUser]);
  React.useEffect(props.dispatchOnAuthListener, []);
  return <>{user.loading ? "Loading.." :"Hello! User"}<>;
}

const mapStateToProps = (state) => {
  return {
    authUser: state.authentication,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    dispatchOnAuthListener: () => dispatch(registerOnAuthListener()),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

-1

การใช้: Apollo Client 2.0, React-Router v4, React 16 (Fiber)

คำตอบที่เลือกใช้ React Router v3 รุ่นเก่า ฉันต้องทำการ "จัดส่ง" เพื่อโหลดการตั้งค่าส่วนกลางสำหรับแอป เคล็ดลับคือการใช้ componentWillUpdate แม้ว่าตัวอย่างจะใช้ไคลเอ็นต์ apollo แต่การดึงโซลูชันไม่เทียบเท่า คุณไม่ต้องการช่อดอกไม้

SettingsLoad.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import {bindActionCreators} from "redux";
import {
  graphql,
  compose,
} from 'react-apollo';

import {appSettingsLoad} from './actions/appActions';
import defQls from './defQls';
import {resolvePathObj} from "./utils/helper";
class SettingsLoad extends Component {

  constructor(props) {
    super(props);
  }

  componentWillMount() { // this give infinite loop or no sense if componente will mount or not, because render is called a lot of times

  }

  //componentWillReceiveProps(newProps) { // this give infinite loop
  componentWillUpdate(newProps) {

    const newrecord = resolvePathObj(newProps, 'getOrgSettings.getOrgSettings.record');
    const oldrecord = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
    if (newrecord === oldrecord) {
      // when oldrecord (undefined) !== newrecord (string), means ql is loaded, and this will happens
      //  one time, rest of time:
      //     oldrecord (undefined) == newrecord (undefined)  // nothing loaded
      //     oldrecord (string) == newrecord (string)   // ql loaded and present in props
      return false;
    }
    if (typeof newrecord ==='undefined') {
      return false;
    }
    // here will executed one time
    setTimeout(() => {
      this.props.appSettingsLoad( JSON.parse(this.props.getOrgSettings.getOrgSettings.record));
    }, 1000);

  }
  componentDidMount() {
    //console.log('did mount this props', this.props);

  }

  render() {
    const record = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
    return record
      ? this.props.children
      : (<p>...</p>);
  }
}

const withGraphql = compose(

  graphql(defQls.loadTable, {
    name: 'loadTable',
    options: props => {
      const optionsValues = {  };
      optionsValues.fetchPolicy = 'network-only';
      return optionsValues ;
    },
  }),
)(SettingsLoad);


const mapStateToProps = (state, ownProps) => {
  return {
    myState: state,
  };
};

const mapDispatchToProps = (dispatch) => {
  return bindActionCreators ({appSettingsLoad, dispatch }, dispatch );  // to set this.props.dispatch
};

const ComponentFull = connect(
  mapStateToProps ,
  mapDispatchToProps,
)(withGraphql);

export default ComponentFull;

App.js

class App extends Component<Props> {
  render() {

    return (
        <ApolloProvider client={client}>
          <Provider store={store} >
            <SettingsLoad>
              <BrowserRouter>
            <Switch>
              <LayoutContainer
                t={t}
                i18n={i18n}
                path="/myaccount"
                component={MyAccount}
                title="form.myAccount"
              />
              <LayoutContainer
                t={t}
                i18n={i18n}
                path="/dashboard"
                component={Dashboard}
                title="menu.dashboard"
              />

2
รหัสนี้ไม่สมบูรณ์และต้องการการตัดส่วนที่ไม่เกี่ยวข้องกับคำถาม
Naoise Golden
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.