This tool allows you to log Redux actions + state to files. It also provides a convenient API for file logging, so that you can add your own loggers (e.g. navigation state).
Use case: QA team can easily create independent logs for each issue, which makes it much easier to understand the root-cause.
3rd party libraries like react-native-fs allows you to write data to files, but each write operation opens & closes a new output stream.
The idea of this library is to use the standard output functions and redirect the output stream to a file, so that the stream remains open. In this case the logging process doesn't affect the app performance
npm install react-native-redux-file-logger
npx pod-install
- Create a configurator for
createReduxFileLoggerMiddleware()
that returns middleware, so that later it can be injected to thestore
import type { Action, AnyAction } from 'redux';
import type { ThunkMiddleware } from 'redux-thunk';
import type { LoggerOptions } from 'react-native-redux-file-logger';
import { Platform } from 'react-native';
export async function configureReduxFileLoggerMiddleware<
State = any,
BasicAction extends Action = AnyAction,
>(): Promise<ThunkMiddleware<State, BasicAction, LoggerOptions<State>> | null> {
if (process.env.NODE_ENV === `development`) {
const {
createReduxFileLoggerMiddleware,
SupportedIosRootDirsEnum,
SupportedAndroidRootDirsEnum,
} = require('react-native-redux-file-logger');
try {
const rootDir =
Platform.OS === 'android' ? SupportedAndroidRootDirsEnum.Files : SupportedIosRootDirsEnum.Cache;
return await createReduxFileLoggerMiddleware(
'redux-action',
{
rootDir,
nestedDir: 'logs',
fileName: 'time-travel.json',
},
{
showDiff: true,
shouldLogPrevState: false,
shouldLogNextState: true,
},
);
} catch (e) {
console.error(e);
}
}
return null;
}
- Create the store and a middleware injector. We can't just pass the middleware to
configureStore()
, because it's created asynchronously.
import { configureStore } from '@reduxjs/toolkit';
import { createMiddlewareInjector } from 'react-native-redux-file-logger';
import counterReducer from './features/counter/slice';
export const store = configureStore({
reducer: { counter: counterReducer },
middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
});
export const middlewareInjector = createMiddlewareInjector<RootState, AppDispatch>(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
- Create Redux file logger middleware and inject it to the
store
.
import * as React from 'react';
import { Provider } from 'react-redux';
import { store, middlewareInjector } from './store';
import { configureReduxFileLoggerMiddleware } from 'react-native-redux-file-logger';
export default function App() {
useEffect(() => {
(async () => {
const rflMiddleware = await configureReduxFileLoggerMiddleware();
if (rflMiddleware) {
middlewareInjector(rflMiddleware);
}
})();
}, []);
return (
<Provider store={store}>
...
</Provider>
);
}
Let's consider an example of a file logger for navigation state changes
- Create file logger for navigation
import { Platform } from 'react-native';
import { addFileLogger, getFileLogger, SupportedAndroidRootDirsEnum, SupportedIosRootDirsEnum } from 'react-native-redux-file-logger';
const rootDir = Platform.OS === 'android' ? SupportedAndroidRootDirsEnum.Files : SupportedIosRootDirsEnum.Cache
await addFileLogger('navigation-state', {
rootDir,
nestedDir: 'logs',
fileName: 'navigation.json'
});
export const navigationStateLogger = getFileLogger(tag);
- Configure navigation state listener
import {createNavigationContainerRef} from '@react-navigation/native';
import {EventArg, EventListenerCallback, EventMapCore} from '@react-navigation/core';
export const navigationRef = createNavigationContainerRef();
export type StateListenerCallbackType = EventListenerCallback<EventMapCore<any>, 'state'>;
export function addNavigationStateListener(listener: StateListenerCallbackType): void {
navigationRef.addListener('state', listener);
}
- Pass
ref
toNavigationContainer
import {navigationRef} from 'path/to/file'
return (
<NavigationContainer ref={navigationRef} >
...
</NavigationContainer>
)
- Use logger inside navigation state listener
import {addStateListener, StateListenerCallbackType} from 'path/to/file'
import {navigationStateLogger} from 'path/to/file'
const stateListener: StateListenerCallbackType = e => {
if (this.isInitialized && e.data.state && e.type) {
navigationStateLogger.log(e.data.state);
}
};
addNavigationStateListener(stateListener);
Archiving logs from all file logger instances to a specified file. If you need to archive logs for a single instance, pass the tag
as a second parameter (see API section).
import { Platform } from 'react-native';
import { archive, SupportedAndroidRootDirsEnum, SupportedIosRootDirsEnum } from 'react-native-redux-file-logger';
const zipFilePath = await archive({
rootDir:
Platform.OS === 'android'
? SupportedAndroidRootDirsEnum.Files
: SupportedIosRootDirsEnum.Cache,
fileName: 'logs.zip',
});
type LoggerOptions<TState = any, TLogger extends {log: (message: string) => void} = Logger> = {
actionInclusionPredicate?: InclusionPredicate<TState>;
diffInclusionPredicate?: InclusionPredicate<TState>;
shouldLogPrevState?: boolean;
shouldLogNextState?: boolean;
showDiff?: boolean;
stateTransformer?: (state: any) => any;
logger: TLogger;
};
- actionInclusionPredicate - actions filtering function, called before middleware logic execution. If returns false, the middleware won't be applied
- actionInclusionPredicate - diffs filtering function
- shouldLogPrevState - whether to add previous state to the file
- shouldLogNextState - whether to add next state to the file
- showDiff - whether to add diff(prev to next) state to the file
- stateTransformer - accepts prev & next state and applies its logic to is
- logger - logger instance, that implements
log(message: string) => void
method
type InclusionPredicate<TState> = (action: AnyAction, getState: () => TState) => boolean;
type FileConfig = {
fileName: string;
nestedDir?: string;
rootDir: SupportedIosRootDirsEnum | SupportedAndroidRootDirsEnum | string;
}
Example:
- rootDir:
/storage/emulated/0/Android/data/com.reduxfileloggerexample/files/
(i.e. SupportedAndroidRootDirsEnum.Files) - nestedDir:
logs
- fileName:
time-travel.json
- Resulting path:
/storage/emulated/0/Android/data/com.reduxfileloggerexample/files/logs/time-travel.json
Dirs that correspond to FileManager.SearchPathDirectory
in Foundation
enum SupportedIosRootDirsEnum {
Downloads = 'Downloads',
Documents = 'Documents',
AppSupportFiles = 'AppSupportFiles',
Cache = 'Cache',
}
Dirs taken from ReactApplicationContext
enum SupportedAndroidRootDirsEnum {
Cache = 'Cache',
Files = 'Files',
}
async function createReduxFileLoggerMiddleware<
State = any,
BasicAction extends Action = AnyAction,
ExtraThunkArg = undefined
>(
tag: string,
fileConfig: FileConfig,
loggerOptions: Omit<LoggerOptions<State>, 'logger'>
): Promise<ThunkMiddleware<State, BasicAction, ExtraThunkArg>> {}
Creates a Redux file logger middleware. Notice, that it doesn't accept logger
, because it's encapsulated
- tag - unique logger identifier
- fileConfig - determines the file path (see above)
- loggerOptions - logger options (see above)
function createLoggerMiddleware<
State = any,
BasicAction extends Action = AnyAction,
ExtraThunkArg = undefined
>(options: LoggerOptions<State>): ThunkMiddleware<State, BasicAction, ExtraThunkArg> {}
Creates a logger middleware. Unlike createReduxFileLoggerMiddleware()
, it accepts a logger
instance, so you can provide your own implementation.
- options - logger options (see above)
const addFileLogger = async (tag: string, fileConfig: FileConfig) => Promise<void>
Creates a unique file logger instance and stores in a map. Use it when you need to add file logger in addition to Redux (e.g. navigation state change, see example above).
- tag - unique logger identifier
- fileConfig - determines the file path (see above)
interface Logger {
log: (message: string) => void;
}
const getFileLogger = (tag: string) => Logger | undefined
Gets a logger instance from map by tag
.
- tag - unique logger identifier
async function archive(fileConfig: FileConfig, tag?: string): Promise<string> {}
Archive logs from all logger instances (or for a specific instance if tag
is provided) to a file. Supports only zip
format for Android. After a successful archive creation the logs are emptied.
- tag - unique logger identifier
- fileConfig - determines the file path (see above)
function createMiddlewareInjector<S = any, D extends Dispatch = Dispatch>(store: MiddlewareAPI<D, S>) {
return function inject(middleware: Middleware) {
store.dispatch = middleware(store)(store.dispatch);
};
}
Create an injector that can be used to add middlewares.
adb root
adb pull /storage/emulated/0/Android/data/com.reduxfileloggerexample/files/example/time-travel.json /Users/{$user}/Desktop
- Copy
archive
result to the clipboard - Finder --> Go --> Go to folder
- Paste the value & hit enter
MIT
Inspired by Oleg Titaev
Made with create-react-native-library