-
-
Save fat/9ab5325ab39acfe242bc7849eb9512c4 to your computer and use it in GitHub Desktop.
nicolas email
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Hey, | |
This is the redux video series that helped me learn how to use it: https://egghead.io/courses/getting-started-with-redux. And here's a section on react "stateless functions": https://facebook.github.io/react/docs/reusable-components.html#stateless-functions | |
Later you might want to play around with React Native | |
React Native for Web: https://github.com/necolas/react-native-web | |
Try it out here: http://codepen.io/necolas/pen/PZzwBR/?editors=0010 | |
And I thought you might get some ideas from what we're doing for mobile.twitter.com, which is a bit like this... | |
Application design | |
This is the kind of directory structure we use to reflect the division of responsibilities. Pretty similar to what you already have. | |
src/ | |
├── api/ | |
├── components/ | |
├── containers/ | |
├── modules/ | |
├── redux/ | |
├── server/ | |
├── screens/ | |
├── client.js | |
├── index.js | |
├── routes.js | |
├── theme.js | |
api | |
The Twitter API client is here. We're moving towards using it on the client like this: | |
// in redux/createMiddleware.js an instance is created | |
import ApiClient from './api' | |
const apiClient = new ApiClient(config) | |
// in redux/modules/*.js the instance is available to thunks | |
const fetchPosts = (params) => (twitterApiClient) => { | |
return twitterApiClient.Users.fetchPosts({ cursor, userId }).then(doShit) | |
} | |
components | |
This is basically your "Bootstrap"-like component library. It doesn't know anything about your app state. Mainly pure components (no internal state). Only imports npm, 'modules', or 'components'. No importing from 'redux'. Probably quite a few components with props that are callbacks, e.g., onPressPlay, which the containers will provide. | |
containers | |
These are also known as "controller views". They glue together UI (components) and data (redux). They might also define a few layout styles and use private, pure components for finishing touches (e.g., adding some spacing between the posts in a list of posts). | |
Our containers are essentially broken into 2 parts that are easy to test separately. | |
1) a 'connect' function that converts redux state to react props | |
// connect.js | |
import * as Users from '../../redux/modules/users' | |
import { connect } from 'react-redux' | |
import logout from '../../modules/logout' | |
// we test these pure functions | |
export const mapStateToProps = (state, props) => { | |
const user = Users.selectById(state, props.userId) | |
return { logout, user } | |
} | |
export const mapDispatchToProps = { | |
fetchUser: Users.fetch | |
} | |
export default connect(mapStateToProps, mapDispatchToProps) | |
2) a component that receives the extra props from 'connect' | |
import connect from './connect' | |
import theme from '../../theme' | |
import UserCard from '../../components/UserCard' | |
import { View } from 'react-native' | |
import React, { Component, PropTypes } from 'react' | |
// we export the class for testing purposes, passing stubs for props | |
export class AccountContainer extends Component { | |
static propTypes = { | |
// these come from 'connect' | |
fetchUser: PropTypes.func.isRequired, | |
logout: PropTypes.func.isRequired, | |
user: PropTypes.object.isRequired, | |
// this is provided by the screen or route handler | |
userId: PropTypes.string.isRequired | |
}; | |
render() { | |
const { user } = this.props; | |
return ( | |
<View className={styles.root}> | |
<UserCard onLongPress={this.handleUserCardLongPress_} user={user} /> | |
<View className={styles.divider} /> | |
<Button onPress={this.handleLogout_}>Logout</Button> | |
</View> | |
); | |
} | |
// the equivalent to binding 'this' to a class method | |
handleLogout_ = (event) => { | |
event.preventDefault() | |
this.props.logout() | |
} | |
handleUserCardLongPress_ = () => { | |
fetchUser(this.props.userId) | |
} | |
} | |
const styles = StyleSheet.create({ | |
root: { | |
padding: '1em' | |
}, | |
divider: { | |
backgroundColor: theme.borderColor, | |
height: 1, | |
marginVertical: '1.5em' | |
} | |
}) | |
// this is the connected class the app renders | |
export default connect(AccountContainer) | |
modules | |
This is where we keep our modules. Some of them could become open source packages, others are quite app-specific. Examples: error handling logic, image processing and resizing, timers, http client, dom utilities. | |
redux | |
All state is managed here, and all means of accessing and modifying state are controlled here. It only imports from 'api' and 'modules'. | |
"Selectors" are used to access data from the store without knowledge of the shape of the store data. "Action creators" are used to trigger changes to the state (which may initiate app re-renders). | |
Here's an example of a basic redux module: | |
// the key to which the reducer is assigned when the store is created using 'combinedReducers' | |
const GLOBAL_STATE_KEY = 'session'; | |
// unique action name prefix | |
const action = (name) => `bumpers/${GLOBAL_STATE_KEY}/${name}`; | |
export const initialState = { | |
guestToken: false, | |
userId: undefined | |
}; | |
export default function reducer(state = initialState, action) { | |
switch (action.type) { | |
case FETCH_GUEST_TOKEN_SUCCESS: | |
return { | |
...state, | |
guestToken: action.payload.guest_token | |
}; | |
default: | |
return state; | |
} | |
} | |
// selectors | |
export const selectGuestToken = (state) => state[GLOBAL_STATE_KEY].guestToken; | |
export const selectLoggedInUserId = (state) => state[GLOBAL_STATE_KEY].userId; | |
// action creators and actions | |
export const FETCH_GUEST_TOKEN_SUCCESS = action('FETCH_GUEST_TOKEN_SUCCESS') | |
export const fetchGuestTokenSuccess = (payload) => ({ payload, type: FETCH_GUEST_TOKEN_SUCCESS }) | |
// this is a thunk (redux-thunk) | |
export const fetchGuestToken = (params) => (dispatch, getState, apiClient) => { | |
return apiClient.Auth.requestGuestToken(params).then( | |
(payload) => dispatch(fetchGuestTokenSuccess(payload)), | |
(errors) => errorLogger(errors) | |
) | |
} | |
screens | |
Screens or scenes – these are top-level components the router delegates rendering to. They combine thins like 'containers' (like nav bar + profile), analytics, setting page meta data like document title (using react-document-meta), and may use url data to seed containers with their props. | |
routes.js | |
Maps routes to screens. | |
client.js | |
Glues together things like 'React.render', the router, routes.js, redux store | |
import { Provider } from 'react-redux' | |
import { browserHistory, Router } from 'some-router' | |
import configureRoutes from './configureRoutes.js' | |
import configureStore from './redux/configureStore' | |
import React from 'react' | |
import ReactDOM from 'react-dom' | |
const store = configureStore(initDataFromServer) | |
const routes = configureRoutes(store) | |
const application = ( | |
<Provider store={store}> | |
<Router history={browserHistory} routes={routes}/> | |
</Provider> | |
) | |
const node = document.getElementById('react-root') | |
ReactDOM.render(application, node) | |
theme.js | |
A JS object with some theme variables for the app – colors, spacing units, font sizes, etc. You could feed bits of this into the postcss loaders (variables, media queries) to make them available as custom variables in css-land. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment