Skip to content

Instantly share code, notes, and snippets.

@Belobobr
Created May 14, 2018 17:03
Show Gist options
  • Select an option

  • Save Belobobr/1257ed1929de6c4d09e28a7fd6f4af0c to your computer and use it in GitHub Desktop.

Select an option

Save Belobobr/1257ed1929de6c4d09e28a7fd6f4af0c to your computer and use it in GitHub Desktop.
Sample arhitecture
//Это описание action с flow ибо немного лень писать бойлерплейт
module.exports = {
actionsDescriptions: {
addFlightToUserRequest: {
requestVariables: 'AddFlightToUserMutationVariables',
scheduledFlight: 'FlightFieldsFragment',
},
addFlightToUserSuccess: {
flight: 'FlightFieldsFragment',
flightType: 'UserFlightEnumType',
},
addFlightToUserFailure: {
error: 'string',
flight: 'FlightFieldsFragment',
flightType: 'UserFlightEnumType',
},
userFlightsRequest: {
requestVariables: 'UserFlightsQueryVariables',
},
userFlightsSuccess: {
flights: 'Array<UserFlightFieldsFragment>',
},
userFlightsFailure: { error: 'string' },
},
importDescription: `
// @flow
import type {
FlightFieldsFragment,
AddFlightToUserMutationVariables,
UserFlightEnumType,
UserFlightFieldsFragment,
UserFlightsQueryVariables,
} from '../../../../graphql/types'
`,
}
//Wee need to use prettier for formatting after generation
const fs = require('fs')
const path = require('path')
const fileNames = process.argv.slice(2)
generateActionsFromDescription(fileNames)
function generateActionsFromDescription() {
let descriptionsDirectory = path.join(__dirname, './descriptions')
let generatedDirectory = path.join(__dirname, './generated')
fs
.readdirSync(descriptionsDirectory)
.filter(file => {
//arguments contains file name
return fileNames.length === 0 || fileNames.indexOf(file) !== -1
})
.forEach(file => {
let { actionsDescriptions, importDescription } = require(path.join(
descriptionsDirectory,
file
))
let {
actionCreators,
actionTypesFlow,
actionCreatorsNames,
actionTypesConstants,
actionTypesFlowNames,
} = generateActionsCreatorsInfo(actionsDescriptions)
let generatedActions =
importDescription +
actionTypesFlow.join('') +
actionCreators.join('') +
generateTypeConstantsExport(actionTypesConstants) +
generateActionCreatorsExport(actionCreatorsNames) +
generateFlowTypesExport(actionTypesFlowNames)
fs.writeFileSync(path.join(generatedDirectory, file), generatedActions)
})
}
function generateActionCreatorsExport(actionCreatorsNames) {
return `export const actions = {
${actionCreatorsNames.join(',')}
}
`
}
function generateFlowTypesExport(actionTypesFlowNames) {
return `export type Actions =
${actionTypesFlowNames.join('|\n')}
`
}
function generateTypeConstantsExport(actionTypesConstants) {
return `export const types = {
${actionTypesConstants.map(
actionTypeConstant => `${actionTypeConstant}: '${actionTypeConstant}'`
)}
};
`
}
function generateActionsCreatorsInfo(actionsDescriptions) {
let actionCreators = []
let actionCreatorsNames = []
let actionTypesConstants = []
let actionTypesFlow = []
let actionTypesFlowNames = []
// {
// setToken: {
// token: ''
// }
// }
Object.keys(actionsDescriptions).forEach(actionName => {
const actionDescription = actionsDescriptions[actionName]
const actionCreatorName = actionName
const actionTypeConstant = splitCamelCase(actionName)
.join('_')
.toUpperCase() //'ACTION_TYPE'
let actionPayload = ''
let actionCreatorsParams = ''
if (actionDescription instanceof Object) {
Object.keys(actionDescription).forEach(varName => {
const varValue = actionDescription[varName]
actionPayload += `${varName}: ${varName},`
actionCreatorsParams += `${varName}: ${varValue}, `
})
}
actionCreators.push(
generateActionCreator(
actionCreatorName,
actionTypeConstant,
actionPayload,
actionCreatorsParams
)
)
actionCreatorsNames.push(actionCreatorName)
let actionTypeFlowName = `${capitalizeFirstLetter(actionCreatorName)}Action`
actionTypesFlow.push(
generateActionFlowType(
actionTypeFlowName,
actionTypeConstant,
actionCreatorsParams
)
)
actionTypesFlowNames.push(actionTypeFlowName)
actionTypesConstants.push(actionTypeConstant)
})
return {
actionCreators,
actionTypesFlow,
actionCreatorsNames,
actionTypesConstants,
actionTypesFlowNames,
}
}
function generateActionFlowType(
actionTypeFlowName,
actionTypeConstant,
actionCreatorsParams
) {
// type SetAuthStatusAction = {
// type: SET_AUTH_STATUS,
// authStatus: string
// }
return `export type ${actionTypeFlowName} = {
type: '${actionTypeConstant}',
${actionCreatorsParams}
}\n`
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
function generateActionCreator(
actionCreatorName,
actionTypeConstant,
actionVariables,
actionCreatorsParams
) {
return `export function ${actionCreatorName}(${actionCreatorsParams}) {
return {
type: '${actionTypeConstant}',
${actionVariables}
}
}\n`
}
function splitCamelCase(splittingString) {
return splittingString.split(/(?=[A-Z])/g)
}
//то что имеет на выходе после обработки description
// @flow
import type {
FlightFieldsFragment,
AddFlightToUserMutationVariables,
UserFlightEnumType,
UserFlightFieldsFragment,
UserFlightsQueryVariables,
} from '../../../../graphql/types'
export type AddFlightToUserRequestAction = {
type: 'ADD_FLIGHT_TO_USER_REQUEST',
requestVariables: AddFlightToUserMutationVariables,
scheduledFlight: FlightFieldsFragment,
}
export type AddFlightToUserSuccessAction = {
type: 'ADD_FLIGHT_TO_USER_SUCCESS',
flight: FlightFieldsFragment,
flightType: UserFlightEnumType,
}
export type AddFlightToUserFailureAction = {
type: 'ADD_FLIGHT_TO_USER_FAILURE',
error: string,
flight: FlightFieldsFragment,
flightType: UserFlightEnumType,
}
export type UserFlightsRequestAction = {
type: 'USER_FLIGHTS_REQUEST',
requestVariables: UserFlightsQueryVariables,
}
export type UserFlightsSuccessAction = {
type: 'USER_FLIGHTS_SUCCESS',
flights: Array<UserFlightFieldsFragment>,
}
export type UserFlightsFailureAction = {
type: 'USER_FLIGHTS_FAILURE',
error: string,
}
export function addFlightToUserRequest(
requestVariables: AddFlightToUserMutationVariables,
scheduledFlight: FlightFieldsFragment
) {
return {
type: 'ADD_FLIGHT_TO_USER_REQUEST',
requestVariables: requestVariables,
scheduledFlight: scheduledFlight,
}
}
export function addFlightToUserSuccess(
flight: FlightFieldsFragment,
flightType: UserFlightEnumType
) {
return {
type: 'ADD_FLIGHT_TO_USER_SUCCESS',
flight: flight,
flightType: flightType,
}
}
export function addFlightToUserFailure(
error: string,
flight: FlightFieldsFragment,
flightType: UserFlightEnumType
) {
return {
type: 'ADD_FLIGHT_TO_USER_FAILURE',
error: error,
flight: flight,
flightType: flightType,
}
}
export function userFlightsRequest(
requestVariables: UserFlightsQueryVariables
) {
return {
type: 'USER_FLIGHTS_REQUEST',
requestVariables: requestVariables,
}
}
export function userFlightsSuccess(flights: Array<UserFlightFieldsFragment>) {
return {
type: 'USER_FLIGHTS_SUCCESS',
flights: flights,
}
}
export function userFlightsFailure(error: string) {
return {
type: 'USER_FLIGHTS_FAILURE',
error: error,
}
}
export const types = {
ADD_FLIGHT_TO_USER_REQUEST: 'ADD_FLIGHT_TO_USER_REQUEST',
ADD_FLIGHT_TO_USER_SUCCESS: 'ADD_FLIGHT_TO_USER_SUCCESS',
ADD_FLIGHT_TO_USER_FAILURE: 'ADD_FLIGHT_TO_USER_FAILURE',
USER_FLIGHTS_REQUEST: 'USER_FLIGHTS_REQUEST',
USER_FLIGHTS_SUCCESS: 'USER_FLIGHTS_SUCCESS',
USER_FLIGHTS_FAILURE: 'USER_FLIGHTS_FAILURE',
}
export const actions = {
addFlightToUserRequest,
addFlightToUserSuccess,
addFlightToUserFailure,
userFlightsRequest,
userFlightsSuccess,
userFlightsFailure,
}
export type Actions =
| AddFlightToUserRequestAction
| AddFlightToUserSuccessAction
| AddFlightToUserFailureAction
| UserFlightsRequestAction
| UserFlightsSuccessAction
| UserFlightsFailureAction
Вкратце.
Есть graphql, из него генерируются flow-types. В reducer'ах используется immutable.js ибо это значительно удобнее
чем Object.assign. Достаточно хорошо добавляет производительности reselect и PureComponent's. Action's с типами очень удобно в
reducer'ах ибо нельзя ошибиться и использовать поля которых нет в action. Ну а писать их руками неохота, поэтому решил написать
небольшой скрипт. C сагами я почти незнаком, но кажется очень удобным ибо может потребоваться какая то сложная логика
вроде того что нужно дождаться завершения двух событий и так далее. Вообще мне хорошо знакома rx-java и насколько знаю
иногда используется в качестве actions ибо позволяет достаточно хорошо выражать сложную логику. Еще используется detox для
тестирования ибо gray тесты более надеждные чем black.
{
"name": "flynotify",
"version": "0.0.1",
"private": true,
"scripts": {
"install:firstTime": "npm install && npm run flow:deps && npm run graphql:introspect_schema && npm run graphql:generate_flow_types && npm run generate:actions",
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"flow": "flow",
"flow:deps": "flow-typed install",
"lint": "eslint .",
"prettier": "eslint . --fix",
"graphql:introspect_schema": "mkdir -p ./graphql/ && apollo-codegen introspect-schema http://85.143.175.131:3003/graphql --output ./graphql/schema.json",
"graphql:generate_flow_types": "apollo-codegen generate ./App/graphql/**/*.js --schema ./graphql/schema.json --add-typename --target flow --output ./graphql/types.js",
"graphql:generate_flow_types_additional": "gql-gen --file ./graphql/schema.json --template flow --out ./graphql/types_additional.js ./App/graphql/**/*.js",
"generate:actions": "mkdir -p ./App/redux/actions/generated/ && node ./App/redux/actions/actionGenerator.js",
"ios:build": "react-native bundle --entry-file='index.js' --bundle-output='./ios/flynotify/main.jsbundle' --dev=false --platform='ios' --assets-dest='./ios/flynotify'"
},
"dependencies": {
"@expo/react-native-action-sheet": "^1.0.2",
"apollo-cache-inmemory": "^1.2.0",
"apollo-client": "^2.3.0",
"apollo-link": "^1.2.2",
"apollo-link-context": "^1.0.5",
"apollo-link-http": "^1.5.4",
"color": "^3.0.0",
"eslint-plugin-react-native": "^3.2.1",
"graphql": "^0.12.3",
"graphql-tag": "^2.9.2",
"immutable": "^4.0.0-rc.9",
"moment": "^2.20.1",
"moment-duration-format": "^2.2.2",
"moment-timezone": "^0.5.16",
"ramda": "^0.25.0",
"react": "16.3.1",
"react-moment": "^0.6.8",
"react-native": "^0.55.3",
"react-native-actionsheet": "^2.4.2",
"react-native-animatable": "^1.2.4",
"react-native-blur": "^3.2.2",
"react-native-calendar-picker": "^5.13.0",
"react-native-calendars": "^1.19.3",
"react-native-dash": "^0.0.8",
"react-native-extended-stylesheet": "^0.8.1",
"react-native-fast-image": "^2.2.5",
"react-native-flexbox-grid": "^0.3.2",
"react-native-gesture-handler": "^1.0.0-alpha.43",
"react-native-i18n": "^2.0.12",
"react-native-interactable": "^0.1.10",
"react-native-keyboard-spacer": "^0.4.1",
"react-native-linear-gradient": "^2.4.0",
"react-native-router-flux": "4.0.0-beta.27",
"react-native-scrollable-tab-view": "^0.8.0",
"react-native-splash-screen": "^3.0.6",
"react-native-tab-view": "0.0.77",
"react-native-vector-icons": "^4.6.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-persist": "^5.8.0",
"redux-saga": "^0.16.0",
"rx-lite": "^4.0.8",
"seamless-immutable": "^7.1.3"
},
"devDependencies": {
"apollo-codegen": "^0.19.1",
"babel-eslint": "^8.2.1",
"babel-jest": "22.0.4",
"babel-preset-react-native": "4.0.0",
"babel-preset-react-native-stage-0": "^1.0.1",
"babel-preset-stage-0": "^6.24.1",
"detox": "^7.3.5",
"eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-config-react-native-prettier": "^1.0.1",
"eslint-plugin-flowtype": "^2.46.3",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-react": "^7.7.0",
"flow-bin": "^0.70.0",
"flow-typed": "^2.4.0",
"graphql-code-generator": "^0.8.21",
"jest": "22.0.4",
"mocha": "^5.1.1",
"prettier": "1.11.1",
"react-test-renderer": "16.0.0",
"reactotron-react-native": "^1.14.0",
"reactotron-redux": "^1.13.0",
"reactotron-redux-saga": "^1.13.0",
"remote-redux-devtools": "^0.5.12"
},
"rnpm": {
"assets": [
"./App/Fonts"
]
},
"jest": {
"preset": "react-native"
},
"detox": {
"configurations": {
"ios.sim.debug": {
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/flynotify.app",
"build": "xcodebuild -project ios/flynotify.xcodeproj -scheme flynotify -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
"type": "ios.simulator",
"name": "iPhone 7"
}
}
}
}
import { Map, List, Record } from 'immutable'
import { types as FlightsTypes } from './../actions/generated/flights'
import type { FlightFieldsFragment } from '../../../graphql/types'
import type { Action } from '../types'
import type { RecordFactory, RecordOf } from 'immutable'
type FlightsStateProps = {
+flightsMap: Map<string, FlightFieldsFragment>,
+trackedFlightsMyIds: List<string>,
+trackedFlightsOtherIds: List<string>,
+userFlightsLoading: boolean,
+userFlightsLoadingError: boolean,
}
export type FlightsState = RecordOf<FlightsStateProps>
export type FlightsDeviation = List<FlightFieldsFragment>
const makeFlightState: RecordFactory<FlightsStateProps> = Record({
flightsMap: Map(),
trackedFlightsMyIds: List(),
trackedFlightsOtherIds: List(),
userFlightsLoading: false,
userFlightsLoadingError: false,
})
const initialState: FlightsState = makeFlightState()
export default function(
state: FlightsState = initialState,
action: Action
): FlightsState {
switch (action.type) {
case FlightsTypes.USER_FLIGHTS_REQUEST: {
return state.set('userFlightsLoading', true)
}
case FlightsTypes.USER_FLIGHTS_SUCCESS: {
const { flights } = action
return state
.set('userFlightsLoading', false)
.set(
'trackedFlightsMyIds',
List(
flights
.filter(flight => flight.type === 'my')
.map(flight => flight.scheduledFlight.id)
)
)
.set(
'trackedFlightsOtherIds',
List(
flights
.filter(flight => flight.type === 'other')
.map(flight => flight.scheduledFlight.id)
)
)
.set(
'flightsMap',
Map(
flights.map(flight => [
flight.scheduledFlight.id,
flight.scheduledFlight,
])
)
)
}
case FlightsTypes.USER_FLIGHTS_FAILURE: {
return state
.set('userFlightsLoading', false)
.set('userFlightsLoadingError', true)
}
case FlightsTypes.ADD_FLIGHT_TO_USER_SUCCESS: {
//TODO сортировать при добавлении?
const { flight, flightType } = action
return state
.update(
'trackedFlightsMyIds',
trackedFlightsMyIds =>
flightType === 'my'
? trackedFlightsMyIds.insert(0, flight.id)
: trackedFlightsMyIds
)
.update(
'trackedFlightsOtherIds',
trackedFlightsOtherIds =>
flightType === 'other'
? trackedFlightsOtherIds.insert(0, flight.id)
: trackedFlightsOtherIds
)
.update('flightsMap', flightsMap => flightsMap.set(flight.id, flight))
}
case FlightsTypes.ADD_FLIGHT_TO_USER_FAILURE: {
const { flight, flightType } = action
return state
.update(
'trackedFlightsMyIds',
trackedFlightsMyIds =>
flightType === 'my'
? trackedFlightsMyIds.pop()
: trackedFlightsMyIds
)
.update(
'trackedFlightsOtherIds',
trackedFlightsOtherIds =>
flightType === 'other'
? trackedFlightsOtherIds.pop()
: trackedFlightsOtherIds
)
.update('flightsMap', flightsMap => flightsMap.remove(flight.id))
}
default:
return state
}
}
//TODO можно тут использовать reselect
export function myFlights(flightState: FlightsState): FlightsDeviation {
const { trackedFlightsMyIds, flightsMap } = flightState
return trackedFlightsMyIds
.map(flightId => flightsMap.get(flightId))
.filter(Boolean)
.filter(flight => flight !== null)
}
export function otherFlights(flightState: FlightsState): FlightsDeviation {
const { trackedFlightsOtherIds, flightsMap } = flightState
return trackedFlightsOtherIds
.map(flightId => flightsMap.get(flightId))
.filter(Boolean)
.filter(flight => flight !== null)
}
// @flow
import { call, put } from 'redux-saga/effects'
import { actions as FlightsActions } from '../actions/generated/flights'
import type { Api } from '../../Services/api'
import type { Saga } from 'redux-saga'
import type { GraphQlResponse } from './../../types'
import type {
AddFlightToUserMutation,
UserFlightsQuery,
} from '../../../graphql/types'
import type {
AddFlightToUserRequestAction,
UserFlightsRequestAction,
} from '../actions/generated/flights'
export function* userFlights(
api: Api,
action: UserFlightsRequestAction
): Saga<void> {
try {
const response: GraphQlResponse<UserFlightsQuery> = yield call(
api.userFlights,
action.requestVariables
)
const { userFlights } = response.data
yield put(FlightsActions.userFlightsSuccess(userFlights))
} catch (error) {
yield put(FlightsActions.userFlightsFailure(error.message))
}
}
export function* addFlightToUser(
api: Api,
action: AddFlightToUserRequestAction
): Saga<void> {
const { scheduledFlight, requestVariables } = action
const flightType = !requestVariables.type ? 'my' : requestVariables.type
try {
yield put(
FlightsActions.addFlightToUserSuccess(scheduledFlight, flightType)
)
const response: GraphQlResponse<AddFlightToUserMutation> = yield call(
api.addFlightToUser,
requestVariables
)
const {
addFlightToUser: { scheduledFlight: responseFlight },
} = response.data
} catch (error) {
yield put(
FlightsActions.addFlightToUserFailure(
error.message,
scheduledFlight,
flightType
)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment