├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── assets └── rn-redux-flipper.gif ├── babel.config.js ├── package.json ├── src ├── components │ └── index.js ├── constants.js ├── detailView │ ├── ActionView.js │ ├── StateView.js │ └── index.js ├── dispatcherView │ └── index.js ├── index.js ├── inspectorView │ └── index.js └── utils.js └── yarn.lock /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: flipper-plugin-react-native-redux-debugger package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | registry-url: https://registry.npmjs.org/ 16 | - run: npm install 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.fontSize": 14 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flipper-plugin-react-native-redux-debugger 2 | Flipper desktop plugin for react native redux logs via [Client Plugin](https://www.npmjs.com/package/redux-middleware-flipper) 3 | 4 | > ❗For Flipper setup in your react native project, please refer to the [setup guide.](https://fbflipper.com/docs/getting-started/react-native) 5 | 6 | ![Workflow](./assets/rn-redux-flipper.gif) 7 | 8 | ## Features ✨ 9 | - Show all dispatched redux actions 10 | - Show details about the action dispatched (action, state diff and the current state) 11 | - Custom action dispatcher 12 | - Replay selected actions 13 | - Search for a specific action type 14 | 15 | ## Setup guide ✍🏻 16 | - Install the `redux-middleware-flipper` and `react-native-flipper`. 17 | 18 | ```bash 19 | yarn add redux-middleware-flipper react-native-flipper 20 | 21 | # for iOS 22 | cd ios && pod install 23 | ``` 24 | 25 | - Add the middleware in dev mode in your redux store setup file. 26 | 27 | ```javascript 28 | if (__DEV__) { 29 | const reduxDebugger = require('redux-middleware-flipper').default; 30 | middleware.push(reduxDebugger()); 31 | } 32 | ``` 33 | 34 | - Open Flipper desktop app and install the plugin. 35 | 36 | ``` 37 | Manage Plugins > Install Plugins > search "RNReduxDebugger" > Install 38 | ``` 39 | 40 | ## Options 41 | ```javascript 42 | if (__DEV__) { 43 | const actionsBlacklist = ['SET_USER_ACCESS_TOKEN']; 44 | const actionsWhitelist = ['GET_USER_PROFILE_SUCCESS']; 45 | const actionReplayDelay = 500; 46 | 47 | const reduxDebugger = require('redux-middleware-flipper').default; 48 | middleware.push(reduxDebugger({ actionsBlacklist, actionsWhitelist, actionReplayDelay })); 49 | } 50 | ``` 51 | 52 | - `actionsBlacklist` - Will not send these action types to Flipper 53 | 54 | - `actionsWhitelist` - Will only send these action types to Flipper 55 | 56 | - `actionReplayDelay` - Delay between multiple actions dispatched via Flipper plugin action replay. Default is *500 ms*. 57 | 58 | ## References 📚 59 | - Getting started with [Flipper](https://fbflipper.com/docs/tutorial/intro) 60 | 61 | ## Motivation 62 | - This project is inspired by [Flutter version](https://github.com/leanflutter/flipper-plugin-reduxinspector) 63 | 64 | ## ISC License (ISC) 65 | Copyright 2020 Aseem Chaudhary 66 | 67 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 68 | 69 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 70 | -------------------------------------------------------------------------------- /assets/rn-redux-flipper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aseemc/flipper-plugin-react-native-redux-debugger/b3a41dc330c14cbe1415794061c73e7d4e6ff533/assets/rn-redux-flipper.gif -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-typescript', 4 | '@babel/preset-react', 5 | ['@babel/preset-env', {targets: {node: 'current'}}] 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://fbflipper.com/schemas/plugin-package/v2.json", 3 | "name": "flipper-plugin-react-native-redux-debugger", 4 | "description": "RNReduxDebugger - Flipper desktop plugin for react native redux logs", 5 | "id": "RNReduxDebugger", 6 | "version": "1.0.8", 7 | "main": "dist/bundle.js", 8 | "flipperBundlerEntry": "src/index.js", 9 | "author": "Aseem Chaudhary", 10 | "homepage": "https://github.com/aseemc/flipper-plugin-react-native-redux-debugger", 11 | "repository": { 12 | "url": "https://github.com/aseemc/flipper-plugin-react-native-redux-debugger", 13 | "type": "git" 14 | }, 15 | "license": "ISC", 16 | "keywords": [ 17 | "flipper", 18 | "react-native-flipper", 19 | "flipper-plugin", 20 | "react-native", 21 | "redux" 22 | ], 23 | "icon": "apps", 24 | "title": "Redux Debugger", 25 | "category": "Redux", 26 | "scripts": { 27 | "lint": "flipper-pkg lint", 28 | "prepack": "flipper-pkg lint && flipper-pkg bundle", 29 | "build": "flipper-pkg bundle", 30 | "watch": "flipper-pkg bundle --watch" 31 | }, 32 | "peerDependencies": { 33 | "antd": "latest", 34 | "flipper": "latest", 35 | "flipper-plugin": "latest" 36 | }, 37 | "devDependencies": { 38 | "@babel/preset-react": "latest", 39 | "@babel/preset-typescript": "latest", 40 | "@types/jest": "latest", 41 | "@types/react": "latest", 42 | "@types/react-dom": "latest", 43 | "antd": "latest", 44 | "flipper": "latest", 45 | "flipper-pkg": "latest", 46 | "flipper-plugin": "latest", 47 | "jest": "latest" 48 | }, 49 | "dependencies": { 50 | "brace": "^0.11.1", 51 | "moment": "^2.29.1", 52 | "react-ace": "^9.2.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import { styled } from 'flipper-plugin'; 2 | import { Text } from 'flipper'; 3 | 4 | export const Header = styled(Text)({ 5 | fontSize: 20, 6 | padding: 10, 7 | fontWeight: 'bold', 8 | }); 9 | 10 | export const TabsContainer = styled.div({ 11 | paddingTop: 10, 12 | paddingBottom: 10, 13 | display: 'flex', 14 | flex: 1, 15 | }); 16 | 17 | export const Spacer = styled.div({ 18 | height: 20, 19 | width: '100%' 20 | }); 21 | 22 | export const DispatchContainer = styled.div({ 23 | display: 'flex', 24 | flexDirection: 'column', 25 | height: 250, 26 | width: '100%', 27 | }); -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const STATE_TABS = { 2 | DIFF: 'Diff', 3 | CURRENT: 'Current', 4 | }; 5 | 6 | export const COLUMN_SIZE = { 7 | timestamp: 150, 8 | actionType: 'flex' 9 | } 10 | 11 | export const COLUMNS = { 12 | timestamp: { 13 | value: '🕰️ Request time' 14 | }, 15 | actionType: { 16 | value: '🧨 Action Type' 17 | }, 18 | time: { 19 | value: '⌛ Duration' 20 | } 21 | } 22 | 23 | export const HEADER_TEXT = { 24 | STATE: 'State', 25 | DISPATCHER: 'Dispatcher', 26 | ACTION: 'Action', 27 | INSPECTOR: 'Inspector', 28 | }; 29 | 30 | export const APP_ID = 'RNReduxDebugger'; -------------------------------------------------------------------------------- /src/detailView/ActionView.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Panel, ManagedDataInspector } from 'flipper'; 3 | import { HEADER_TEXT } from '../constants'; 4 | 5 | const ActionView = ({ action }) => ( 6 | 7 | 8 | 9 | ) 10 | 11 | export default memo(ActionView); 12 | -------------------------------------------------------------------------------- /src/detailView/StateView.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from 'react'; 2 | import { 3 | Panel, 4 | DataDescription, 5 | ManagedDataInspector, 6 | Tab, 7 | Tabs, 8 | } from 'flipper'; 9 | import { TabsContainer } from '../components'; 10 | import { STATE_TABS, HEADER_TEXT } from '../constants'; 11 | 12 | const StateView = ({ nextState, prevState }) => { 13 | const [activeStateTab, setActiveStateTab] = useState(STATE_TABS.DIFF); 14 | 15 | return ( 16 | 17 | 18 | setActiveStateTab(key)} 22 | > 23 | 24 | { 25 | typeof nextState !== 'object' 26 | ? 27 | : 28 | } 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | export default memo(StateView); 40 | -------------------------------------------------------------------------------- /src/detailView/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { Header } from '../components'; 4 | import ActionView from './ActionView'; 5 | import StateView from './StateView'; 6 | 7 | const DetailView = (props) => { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default DetailView; 17 | -------------------------------------------------------------------------------- /src/dispatcherView/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useRef } from 'react'; 2 | import { 3 | Panel, 4 | Button, 5 | } from 'flipper'; 6 | import "brace"; 7 | import AceEditor from "react-ace"; 8 | import "brace/mode/json"; 9 | import "brace/theme/chrome"; 10 | 11 | import { Spacer, DispatchContainer } from '../components'; 12 | import { validateJson } from '../utils'; 13 | 14 | const DispatcherView = ({ client }) => { 15 | const [newAction, setNewAction] = useState({}); 16 | 17 | const handleDispatch = async () => { 18 | const validJson = validateJson(newAction); 19 | if (validJson) { 20 | await client.send('dispatch', validJson); 21 | } else { 22 | alert('Invalid action.') 23 | } 24 | } 25 | 26 | return ( 27 | 28 | 29 | 43 | 44 | 51 | 52 | 53 | ) 54 | } 55 | 56 | export default memo(DispatcherView); 57 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { usePlugin, createState, useValue, Layout } from 'flipper-plugin'; 3 | import { DetailSidebar } from 'flipper'; 4 | 5 | import DetailView from './detailView'; 6 | import DispatcherView from './dispatcherView'; 7 | import InspectorView from './inspectorView'; 8 | 9 | const clientRef = React.createRef(); 10 | 11 | export const plugin = (client) => { 12 | const data = createState({}, { persist: 'data' }); 13 | clientRef.current = client; 14 | 15 | client.onMessage('action', (newActionLog) => { 16 | data.update((draft) => { 17 | draft[newActionLog.id] = newActionLog; 18 | }); 19 | }); 20 | 21 | return { data }; 22 | } 23 | 24 | export const Component = () => { 25 | const instance = usePlugin(plugin); 26 | const data = useValue(instance.data); 27 | const [detailViewRowId, setDetailViewRowId] = useState(); 28 | 29 | const showDetailView = () => { 30 | if (detailViewRowId) { 31 | return 32 | } 33 | 34 | return null; 35 | } 36 | 37 | return ( 38 | 39 | 45 | {showDetailView()} 46 | 47 | 48 | ); 49 | } -------------------------------------------------------------------------------- /src/inspectorView/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Text, SearchableTable, Button, Panel } from 'flipper'; 3 | import moment from 'moment'; 4 | 5 | import { COLUMN_SIZE, COLUMNS, APP_ID, HEADER_TEXT } from '../constants'; 6 | 7 | export const InspectorView = ({ client, instance, data, setDetailViewRowId }) => { 8 | const [selectedIds, setSelectedIds] = useState(); 9 | 10 | const buildRow = (row) => { 11 | const { id, requestTime, action: { type }, duration } = row; 12 | return { 13 | columns: { 14 | timestamp: { 15 | value: {moment(requestTime).format('HH:mm:ss.SSS')}, 16 | filterValue: requestTime 17 | }, 18 | actionType: { 19 | value: {type}, 20 | filterValue: type 21 | }, 22 | time: { 23 | value: {duration}, 24 | filterValue: duration 25 | } 26 | }, 27 | key: id, 28 | copyText: JSON.stringify(row), 29 | filterValue: type 30 | } 31 | } 32 | 33 | const clearData = () => { 34 | setDetailViewRowId(); 35 | setSelectedIds(); 36 | instance.data.set({}); 37 | }; 38 | 39 | const handleRowHighlighted = (rowIds) => { 40 | if (rowIds && rowIds.length === 1) { 41 | setDetailViewRowId(rowIds[0]); 42 | } else { 43 | setDetailViewRowId(); 44 | } 45 | 46 | setSelectedIds(rowIds); 47 | }; 48 | 49 | const handleActionReplay = async () => { 50 | try { 51 | const sortedActions = selectedIds.sort(); 52 | const actions = sortedActions.map(id => data[id].action); 53 | await client.send('dispatch', actions); 54 | } catch (error) { 55 | alert('Invalid action replay'); 56 | } 57 | } 58 | 59 | return ( 60 | 61 | 73 | 74 | 75 | 76 | )} 77 | multiHighlight 78 | /> 79 | 80 | ); 81 | } 82 | 83 | export default InspectorView; -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const formatTimestamp = (timestamp) => { 2 | const time = new Date(timestamp); 3 | const hours = time.getHours(); 4 | const minutes = time.getMinutes(); 5 | const seconds = time.getSeconds(); 6 | const milliSeconds = time.getMilliseconds(); 7 | 8 | return `${hours}:${minutes}:${seconds}.${milliSeconds}`; 9 | } 10 | 11 | export const validateJson = (value) => { 12 | try { 13 | const json = JSON.parse(value); 14 | if (Object.keys(json).length) return json; 15 | } catch (e) { } 16 | 17 | return null; 18 | } --------------------------------------------------------------------------------