Redux Requests Factory
npm install redux-requests-factory // or npm install redux-requests-factory --save
Table of Contents
-
Redux Requests Factory
-
Requests Factory Config
-
-
config.stateRequestKey
-
-
-
config.serializeRequestParameters
-
-
-
config.debounceOptions
-
config.stringifyParamsForDebounce
-
-
-
config.transformError
-
config.transformResponse
-
-
-
config.rejectedActions
-
config.fulfilledActions
-
-
-
config.includeInGlobalLoading
-
-
-
Requests Factory Instance
- Requests Factory Instance Actions
-
-
isSomethingLoadingSelector
-
- Create Redux Requests Factory
-
Requests Factory Config
Examples
Installation
redux-requests-factory
is available on npm
.
npm install redux-requests-factory
store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'; import { stateRequestsKey, requestsReducer, createRequestsFactoryMiddleware, } from 'redux-requests-factory'; export const reducer = combineReducers({ [stateRequestsKey]: requestsReducer, // ... others reducers }); const { middleware: requestsFactoryMiddleware } = createRequestsFactoryMiddleware(); const reduxMiddleware = applyMiddleware(requestsFactoryMiddleware); const store = createStore(reducer, reduxMiddleware); export default store;
Usage
import { requestsFactory } from 'redux-requests-factory'; const loadUsersRequest = () => fetch('https://mysite.com/users').then(res => res.json()); export const { // actions loadDataAction, // do request once (can be dispatched many times, but do request once) forcedLoadDataAction, // do request every time (used when need reload data) doRequestAction, // do request every time (used for create, update and delete requests) cancelRequestAction, // cancel request requestFulfilledAction, // dispatched when request fulfilled requestRejectedAction, // dispatched when request rejected setErrorAction, // set custom Error for this request (requestRejectedAction will be dispatched) setResponseAction, // set response for this request (requestFulfilledAction will be dispatched) resetRequestAction, // reset request data // selectors responseSelector, // returns `response || []` errorSelector, // returns Error when request rejected or undefined requestStatusSelector, // returns request status ('none', 'loading', 'success', 'failed', 'canceled') isLoadingSelector, // returns true when request status === 'loading' isLoadedSelector, // returns true when request status === 'success' } = requestsFactory({ request: loadUsersRequest, stateRequestKey: 'users', transformResponse: (response) => response || [], });
import React from 'react'; import { useSelector } from 'react-redux'; import { isSomethingLoadingSelector } from 'redux-requests-factory'; const App = () => { const isSomethingLoading = useSelector(isSomethingLoadingSelector); // returns true when something loads return ( <> {isSomethingLoading ? <div>'Something Loading...'</div> : null} </> ); }
Example
requests/users.js
import { requestsFactory } from 'redux-requests-factory'; const loadUsersRequest = () => fetch('https://mysite.com/users').then(res => res.json()); export const { loadDataAction: loadUsersAction, // do request once forcedLoadDataAction: forcedLoadUsersAction, // do request every time cancelRequestAction: cancelLoadUsersAction, responseSelector: usersSelector, // return `response || []` errorSelector: loadUsersErrorSelector, isLoadingSelector: isLoadingUsersSelector, isLoadedSelector: isLoadedUsersSelector, } = requestsFactory({ request: loadUsersRequest, stateRequestKey: 'users', transformResponse: (response) => response || [], });
requests/posts-by-user.js
import { requestsFactory } from 'redux-requests-factory'; const loadUserPostsRequest = ({ userId }) => fetch( `https://mysite.com/posts?userId=${userId}` ).then(res => res.json()); export const { loadDataAction: loadUserPostsAction, forcedLoadDataAction: forcedLoadUserPostsAction, setResponseAction: setUserPostsAction, responseSelector: userPostsSelector, // return function `({ userId }) => response || []` } = requestsFactory({ request: loadUserPostsRequest, stateRequestKey: 'user-posts', useDebounce: true, serializeRequestParameters: ({ userId }) => `${userId}`, // selector will return function transformResponse: (response) => response || [], });
requests/add-post.js
import { requestsFactory } from 'redux-requests-factory'; import { setUserPostsAction, userPostsSelector } from './posts-by-user'; const addPostRequest = ({ userId, title, body }) => fetch('https://mysite.com/posts', { method: 'POST', body: JSON.stringify({ title, body, userId, }), headers: { 'Content-type': 'application/json; charset=UTF-8', }, }).then(res => res.json()); export const { doRequestAction: addPostAction, cancelRequestAction: cancelAddPostAction, isLoadingSelector: isLoadingAddPostSelector, } = requestsFactory({ request: addPostRequest, stateRequestKey: 'add-post', includeInGlobalLoading: false, // not include in isSomethingLoadingSelector serializeRequestParameters: ({ userId }) => `${userId}`, fulfilledActions: [ // this actions calls when addPostRequest fulfilled ({ response, request: { userId }, state }) => { return setUserPostsAction({ response: [...userPostsSelector(state)({ userId }), response], params: { userId }, }); }, ], });
App.js
import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { isSomethingLoadingSelector } from 'redux-requests-factory'; import { loadUsersAction, forcedLoadUsersAction, cancelLoadUsersAction, usersSelector, } from '../requests/users'; import { loadUserPostsAction, userPostsSelector, forcedLoadUserPostsAction, } from '../requests/posts-by-user'; import { addPostAction, isLoadingAddPostSelector, cancelAddPostAction, } from '../requests/add-post'; const App = () => { const dispatch = useDispatch(); const users = useSelector(usersSelector); const postsByUser = useSelector(userPostsSelector); const isSomethingLoading = useSelector(isSomethingLoadingSelector); const isLoadingAddPost = useSelector(isLoadingAddPostSelector); const onLoadUsers = useCallback(() => dispatch(loadUsersAction()), [ dispatch, ]); const onForcedLoadUsers = useCallback( () => dispatch(forcedLoadUsersAction()), [dispatch] ); const onCancelLoadUsers = useCallback( () => dispatch(cancelLoadUsersAction()), [dispatch] ); const onLoadUserPosts = useCallback( (userId) => dispatch(loadUserPostsAction({ userId })), [dispatch] ); const onForcedLoadUserPosts = useCallback( (event) => { dispatch( forcedLoadUserPostsAction({ userId: event.currentTarget.dataset.userId, }) ); }, [dispatch] ); const onAddPost = useCallback( (even) => { event.preventDefault(); const form = event.currentTarget; const elements = form.elements; const userId = form.dataset.userId; dispatch(cancelAddPostAction({ userId })); dispatch( addPostAction({ userId, title: elements.title.value, body: elements.body.value, }) ); }, [dispatch] ); useEffect(() => { onLoadUsers(); }, [onLoadUsers]); useEffect(() => { if (users) { users.forEach(({ id }) => { onLoadUserPosts(id); }); } }, [users, onLoadUserPosts]); return ( <div> <div> {isSomethingLoading ? 'Something Loading...' : null} </div> <button onClick={onLoadUsers}> Load Users </button> <button onClick={onForcedLoadUsers}> Forced Load Users </button> <button onClick={onCancelLoadUsers}> Cancel Load Users </button> <ul> {users.map(({ id, name }) => ( <li key={id}> {name} <ul> {postsByUser({ userId: id }).map(({ id, title }, index) => ( <li key={`${id}_${index}`}>{title}</li> ))} <button data-user-id={id} onClick={onForcedLoadUserPosts} > Forced Load User Posts With Debounce 500ms </button> <form data-user-id={id} onSubmit={onAddPost} > <h3>Add new post </h3> <label> Title <input id={`title_${id}`} name="title" /> </label> <label> Body <textarea name="body" /> </label> <button type="submit" disabled={isLoadingAddPost({ userId: id })} > {isLoadingAddPost({ userId: id }) ? 'Loading...' : 'Add'} </button> </form> </ul> </li> ))} </ul> </div> ); }; export default App;
Requests Factory Config
import { requestsFactory } from 'redux-requests-factory'; const {...} = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', serializeRequestParameters: ({ id }) => `${id}`, // Debounce useDebounce: true, debounceWait: 500, stringifyParamsForDebounce: ({ id }) => `${id}`, debounceOptions: { leading: true, trailing: false, maxWait: 500, }, // Request rejected transformError: (error) => error && error.message, rejectedActions: [({ error, request: { id }, state }) => { // return the actions that should be dispatched when request is rejected return { type: 'SHOW_NOTIFICATION' } }], // Request fulfilled transformResponse: (response) => response || {}, fulfilledActions: [({ response, request: { id }, state }) => { // return the actions that should be dispatched when request is fulfilled return { type: 'SHOW_NOTIFICATION' } }], // Loading includeInGlobalLoading: true, });
Required
config.request
request
is required
field, it is should be function that takes parameters (or not) and returns Promise
with params:
const { doRequestAction, forcedLoadDataAction, loadDataAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); doRequestAction({ id: 1 }); // or forcedLoadDataAction({ id: 1 }); // or loadDataAction({ id: 1 });
or without params:
const { doRequestAction, forcedLoadDataAction, loadDataAction, } = requestsFactory({ request: () => fetch('https://mysite.com/api/users').then(res => res.json()), stateRequestKey: 'users', }); doRequestAction(); // or forcedLoadDataAction(); // or loadDataAction();
config.stateRequestKey
stateRequestKey
is required
field, it is should be unique string key between all requests
const {...} = requestsFactory({ stateRequestKey: 'users', }); const {...} = requestsFactory({ stateRequestKey: 'user-posts', }); // Now the state is like here // state = { // [stateRequestsKey]: { // responses: { // users: { // status: RequestsStatuses.None, // response: undefined, // error: undefined, // }, // 'user-posts': { // status: RequestsStatuses.None, // response: undefined, // error: undefined, // }, // }, // }, // };
Serialize
config.serializeRequestParameters
serializeRequestParameters
is not required
field, it is should be function that takes parameters and returns string
.
When used serializeRequestParameters
all selectors return function that takes parameters and returns selected value.
When used serializeRequestParameters
params are required for all actions.
const { loadDataAction, forcedLoadDataAction, doRequestAction, cancelRequestAction, requestFulfilledAction, requestRejectedAction, setErrorAction, setResponseAction, resetRequestAction, responseSelector, errorSelector, requestStatusSelector, isLoadingSelector, isLoadedSelector, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', serializeRequestParameters: ({ id }) => `${id}`, }); loadDataAction({ id: 1 }); forcedLoadDataAction({ id: 1 }); doRequestAction({ id: 1 }); cancelRequestAction({ id: 1 }); setErrorAction({ error: new Error(), params: { id: 1 }, }); setResponseAction({ response: { id: 1, name: 'Name' }, params: { id: 1 }, }); resetRequestAction({ id: 1 }); responseSelector(store)({ id: 1 }); errorSelector(store)({ id: 1 }); requestStatusSelector(store)({ id: 1 }); isLoadingSelector(store)({ id: 1 }); isLoadedSelector(store)({ id: 1 }); // loadDataAction({ id: 1 }); // loadDataAction({ id: 2 }); // loadDataAction({ id: 3 }); // // Now the state is like here // state = { // [stateRequestsKey]: { // responses: { // user: { // '1': { // status: RequestsStatuses.Loading, // response: undefined, // error: undefined, // }, // '2': { // status: RequestsStatuses.Loading, // response: undefined, // error: undefined, // }, // '3': { // status: RequestsStatuses.Loading, // response: undefined, // error: undefined, // }, // }, // }, // }, // };
Debounce
config.useDebounce
useDebounce
is not required
field, default value - false
.
When useDebounce: true
requestsFactory creates debounced actions doRequestAction
, forcedLoadDataAction
and loadDataAction
that delays dispatch action with same params
until after wait config.debounceWait
milliseconds have elapsed since the last time the debounced action was dispatched.
Detect same params helps config.stringifyParamsForDebounce
.
For debounce used lodash.debounce
and you can use own debounce options config.debounceOptions
.
const {...} = requestsFactory({ useDebounce: true, });
config.debounceWait
debounceWait
is not required
field, default value - 500
.
Used when config.useDebounce: true
.
const {...} = requestsFactory({ debounceWait: 300, });
config.debounceOptions
debounceOptions
is not required
field, default value:
{ leading: true, trailing: false, maxWait: config.debounceWait, }
It is options for lodash.debounce
.
Used when config.useDebounce: true
.
const {...} = requestsFactory({ debounceOptions: { leading: true, trailing: false, maxWait: 300, }, });
config.stringifyParamsForDebounce
stringifyParamsForDebounce
is not required
field, default value - JSON.stringify
. It is should be function that takes parameters and returns string
.
Used when config.useDebounce: true
.
const {...} = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', stringifyParamsForDebounce: ({ id }) => `${id}`, });
Transform
config.transformError
transformError
is not required
field, it is should be function that takes request error
or undefined
and returns transformed error
or undefined
. transformError
used for errorSelector
.
const { errorSelector, } = requestsFactory({ transformError: (error) => error && `Error: ${error.message}`, }); errorSelector(state); // undefined or `Error: ${error.message}`
config.transformResponse
transformResponse
is not required
field, it is should be function that takes request response
or undefined
and returns transformed response
. transformResponse
used for responseSelector
. Better use transformResponse
for setting default value.
NOTE: For best performance, do not use transformResponse
with serializeRequestParameters
for expensive transformations. For all expensive transforms better use reselect
.
const { responseSelector, } = requestsFactory({ transformResponse: (response) => response || [],, }); responseSelector(state); // []
Actions
config.rejectedActions
rejectedActions
is not required
field, default value - []
. It is should be array with actions or with functions that takes object { error, request, state }
as parameter and returns action
or [action, action, ...]
that will be dispatched when request is rejected.
const {...} = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', rejectedActions: [ { type: 'SHOW_NOTIFICATION' }, // simple action ({ error, request: { id }, state }) => { // ... return id === 1 ? { type: 'SHOW_ERROR' } : null; }, // function that returns an action or null ({ error, request: { id }, state }) => { // ... return [{ type: 'SHOW_ERROR' }, (id === 1 ? { type: 'SHOW_ERROR' } : null) ]; }, // function that returns an array with actions or null ], });
config.fulfilledActions
fulfilledActions
is not required
field, default value - []
. It is should be array with actions or with functions that takes object { response, request, state }
as parameter and returns action
or [action, action, ...]
that will be dispatched when request is fulfilled.
const {...} = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', fulfilledActions: [ { type: 'SHOW_NOTIFICATION' }, // simple action ({ response, request: { id }, state }) => { // ... return !response ? { type: 'SHOW_ERROR' } : null; }, // function that returns an action or null ({ response, request: { id }, state }) => { // ... return [{ type: 'SHOW_NOTIFICATION' }, (!response ? { type: 'SHOW_ERROR' } : null) ]; }, // function that returns an array with actions or null ], });
Global Loading
config.includeInGlobalLoading
includeInGlobalLoading
is not required
field, default value - true
. It is should be boolean.
When includeInGlobalLoading: true
and request is loading, global isSomethingLoadingSelector
will be return true
. If includeInGlobalLoading: false
you can use isLoadingSelector
import { isSomethingLoadingSelector } from 'redux-requests-factory'; const { isLoadingSelector, } = requestsFactory({ includeInGlobalLoading: false, });
Requests Factory Instance
import { requestsFactory } from 'redux-requests-factory'; export const { // actions loadDataAction, // do request once (can be dispatched many times, but do request once) forcedLoadDataAction, // do request every time (used when need reload data) doRequestAction, // do request every time (used for create, update and delete requests) cancelRequestAction, // cancel request requestFulfilledAction, // dispatched when request fulfilled requestRejectedAction, // dispatched when request rejected setErrorAction, // set custom Error for this request (requestRejectedAction will be dispatched) setResponseAction, // set response for this request (requestFulfilledAction will be dispatched) resetRequestAction, // reset request data // selectors responseSelector, // returns `response || []` errorSelector, // returns Error when request rejected or undefined requestStatusSelector, // returns request status ('none', 'loading', 'success', 'failed', 'canceled') isLoadingSelector, // returns true when request status === 'loading' isLoadedSelector, // returns true when request status === 'success' } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', transformResponse: (response) => response || [], });
Requests Factory Instance Actions
loadDataAction
loadDataAction
do request once (can be dispatched many times, but do request once)
export const { loadDataAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); dispatch(loadDataAction({ id: 1 }));
forcedLoadDataAction
forcedLoadDataAction
do request every time (used when need reload data)
export const { forcedLoadDataAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); dispatch(forcedLoadDataAction({ id: 1 }));
doRequestAction
doRequestAction
do request every time (used for create, update and delete requests)
export const { doRequestAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); dispatch(doRequestAction({ id: 1 }));
cancelRequestAction
cancelRequestAction
cancel active request
export const { cancelRequestAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); dispatch(cancelRequestAction());
requestFulfilledAction
requestFulfilledAction
dispatched with payload: { params, response }
when request fulfilled. Can be used for subscriptions ( redux-observable
, redux-saga
).
import { ofType } from 'redux-observable'; export const { requestFulfilledAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); const loadUserFulfilledEpic = (action$, state$) => action$.pipe( ofType(requestFulfilledAction), tap(({ payload: { params: { id }, response } }) => { alert(`User ${id} is loaded`); }), ignoreElements() );
requestRejectedAction
requestRejectedAction
dispatched with payload: { params, error }
when request rejected. Can be used for subscriptions ( redux-observable
, redux-saga
).
import { ofType } from 'redux-observable'; export const { requestRejectedAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); const loadUserRejectedEpic = (action$, state$) => action$.pipe( ofType(requestRejectedAction), tap(({ payload: { params: { id }, error } }) => { alert(`User ${id} is not loaded`); }), ignoreElements() );
setErrorAction
setErrorAction
set custom Error for this request ( requestRejectedAction
will be dispatched)
export const { setErrorAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); dispatch(setErrorAction({ error: 'some error' }));
setResponseAction
setResponseAction
set response for this request ( requestFulfilledAction
will be dispatched)
export const { setResponseAction, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); dispatch(setResponseAction({ response: { id: 1, name: 'Name' } }));
resetRequestAction
resetRequestAction
reset request data. Set undefined
to response
and error
, and set RequestsStatuses.None
to status
.
export const { resetRequestAction, responseSelector, errorSelector, requestStatusSelector, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); dispatch(resetRequestAction()); responseSelector(state); // undefined errorSelector(state); // undefined requestStatusSelector(state); // RequestsStatuses.None
Selectors
responseSelector
responseSelector
returns response
when request fulfilled or undefined
export const { responseSelector, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); responseSelector(state);
errorSelector
errorSelector
returns Error
when request rejected or undefined
export const { errorSelector, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); errorSelector(state);
requestStatusSelector
requestStatusSelector
returns request status
( RequestsStatuses.None
, RequestsStatuses.Loading
, RequestsStatuses.Success
, RequestsStatuses.Failed
, RequestsStatuses.Canceled
)
import { RequestsStatuses } from 'redux-requests-factory'; export const { requestStatusSelector, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); requestStatusSelector(state) === RequestsStatuses.None;
isLoadingSelector
isLoadingSelector
returns true when request status === RequestsStatuses.Loading
export const { isLoadingSelector, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); isLoadingSelector(state);
isLoadedSelector
isLoadedSelector
returns true when request status === RequestsStatuses.Success
export const { isLoadedSelector, } = requestsFactory({ request: ({ id }) => fetch(`https://mysite.com/api/user/${id}`).then(res => res.json()), stateRequestKey: 'user', }); isLoadedSelector(state);
Global Selectors
isSomethingLoadingSelector
isSomethingLoadingSelector
returns true
when something loads
import { isSomethingLoadingSelector } from 'redux-requests-factory'; isSomethingLoadingSelector(state);
Create Redux Requests Factory
Used if you need more than one instance of createReduxRequestsFactory
import createReduxRequestsFactory from 'redux-requests-factory'; export const { stateRequestsKey, // 'api-key-one' createRequestsFactoryMiddleware, // Middleware for 'api-key-one' requestsFactory, // requestsFactory for 'api-key-one' requestsReducer, // requestsReducer for 'api-key-one' isSomethingLoadingSelector, // isSomethingLoadingSelector for 'api-key-one' } = createReduxRequestsFactory({ stateRequestsKey: 'api-key-one', }); export const { stateRequestsKey, // 'api-key-two' createRequestsFactoryMiddleware, // Middleware for 'api-key-two' requestsFactory, // requestsFactory for 'api-key-two' requestsReducer, // requestsReducer for 'api-key-two' isSomethingLoadingSelector, // isSomethingLoadingSelector for 'api-key-two' } = createReduxRequestsFactory({ stateRequestsKey: 'api-key-two', });
SSR
store.js
const makeStore = (initialState) => { const { middleware, toPromise } = createRequestsFactoryMiddleware(); const reduxMiddleware = applyMiddleware(middleware); const store = createStore(reducer, initialState, reduxMiddleware); store.asyncRequests = toPromise; return store; };
app.js
const loadData = async ({ isServer, store }) => { store.dispatch(loadUsersAction()); if (isServer) { await store.asyncRequests(); } }
TypeScript
Full support TypeScript
TypeScript + create-react-app + redux-requests-factory
TypeScript + next.js + redux-requests-factory
License
MIT
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
乔布斯离开了,马斯克来了
[日]竹内一正 / 干太阳 / 中信出版社 / 2015-11
在电动汽车的创新上,特斯拉抓住了一个群体的独特需求,外形很酷,不烧油,智能化控制。所有的颠覆式创新都不是敲锣打鼓来的,而是隐藏在一片噪声里,马斯克给我们带来的特斯拉虽然不尽完美,但他做产品的思维和执着于未来的勇气,值得学习。埃隆•马斯克创办公司也不是为了赚钱,而是为了拯救人类和地球,电动汽车、太阳能发电、宇宙火箭,不管是哪一项都足以令一个国家付出巨大的代价去研究开发,但埃隆•马斯克却一个人在做这些......一起来看看 《乔布斯离开了,马斯克来了》 这本书的介绍吧!