├── .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 | 
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 | }
--------------------------------------------------------------------------------