Skip to content

Commit

Permalink
fix(react-native-storybook): App or Storybook mode update (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
dawidk92 authored Jul 26, 2024
1 parent 49a3ed9 commit 150fb2c
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 141 deletions.
4 changes: 2 additions & 2 deletions examples/expo-example/builds/development/android.apk
Git LFS file not shown
4 changes: 2 additions & 2 deletions examples/expo-example/builds/development/ios.tar.gz
Git LFS file not shown
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import android.content.Context;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.OutputStream;
Expand All @@ -17,16 +20,29 @@
import java.util.HashMap;

public class RNSherloModule extends ReactContextBaseJavaModule {
private static final String RNExternalDirectoryPath = "RNExternalDirectoryPath";
public static final String RNSHERLO = "RNSherlo";
private static final String CONFIG_FILENAME = "config.sherlo";

private final ReactApplicationContext reactContext;

private static String appOrStorybookMode = "app"; // Static variable to hold the mode throughout the app lifecycle
private static String sherloDirectoryPath = "";

public RNSherloModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;

// Set Sherlo directory path
File externalDirectory = this.getReactApplicationContext().getExternalFilesDir(null);
if (externalDirectory != null) {
this.sherloDirectoryPath = externalDirectory.getAbsolutePath() + "/sherlo";
}

// If it's running on Sherlo server set Storybook mode
String configPath = this.sherloDirectoryPath + "/" + CONFIG_FILENAME;
if (new File(configPath).isFile()) {
this.appOrStorybookMode = "storybook";
}
}

@Override
Expand All @@ -35,15 +51,26 @@ public String getName() {
}

@ReactMethod
public void setAppOrStorybookMode(String appOrStorybookMode, Promise promise) {
public void setAppOrStorybookModeAndRestart(String appOrStorybookMode, Promise promise) {
this.appOrStorybookMode = appOrStorybookMode;

// Restart JS
getReactInstanceManager().recreateReactContextInBackground();
promise.resolve(null);
}

@ReactMethod
public void getAppOrStorybookMode(Promise promise) {
promise.resolve(this.appOrStorybookMode);
}

private ReactInstanceManager getReactInstanceManager() {
Context context = getReactApplicationContext().getApplicationContext();
if (context instanceof ReactApplication) {
return ((ReactApplication) context).getReactNativeHost().getReactInstanceManager();
}
return null;
}


private void reject(Promise promise, String filepath, Exception ex) {
Expand Down Expand Up @@ -125,20 +152,6 @@ public void readFile(String filepath, Promise promise) {
}
}

@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();

File externalDirectory = this.getReactApplicationContext().getExternalFilesDir(null);
if (externalDirectory != null) {
constants.put(RNExternalDirectoryPath, externalDirectory.getAbsolutePath());
} else {
constants.put(RNExternalDirectoryPath, null);
}

return constants;
}

private OutputStream getOutputStream(String filepath, boolean append) throws IORejectionException {
Uri uri = getFileUri(filepath, false);
OutputStream stream;
Expand Down Expand Up @@ -205,4 +218,14 @@ private static byte[] getInputStreamBytes(InputStream inputStream) throws IOExce
}
return bytesResult;
}

@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();

constants.put("appOrStorybookMode", this.appOrStorybookMode);
constants.put("sherloDirectoryPath", this.sherloDirectoryPath);

return constants;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package io.sherlo.storybookreactnative;

import java.util.Arrays;
Expand Down Expand Up @@ -27,4 +26,4 @@ public List<Class<? extends JavaScriptModule>> createJSModules() {
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
}
30 changes: 27 additions & 3 deletions packages/react-native-storybook/ios/RNSherlo.m
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
#import "RNSherlo.h"
#import <Foundation/Foundation.h>
#import <React/RCTUtils.h>
#import <React/RCTUIManager.h>
#if __has_include(<React/RCTUIManagerUtils.h>)
#import <React/RCTUIManagerUtils.h>
#import <React/RCTUIManagerUtils.h>
#endif
#import <React/RCTBridge.h>

static NSString *appOrStorybookMode = @"app"; // Static variable to hold the mode throughout the app lifecycle
static NSString *sherloDirectoryPath = @"";
static NSString *CONFIG_FILENAME = @"config.sherlo";

@implementation RNSherlo

RCT_EXPORT_MODULE()

@synthesize bridge = _bridge;

- (instancetype)init
{
self = [super init];
if (self) {
// Set Sherlo directory path
sherloDirectoryPath = [[self getPathForDirectory:NSDocumentDirectory] stringByAppendingPathComponent:@"sherlo"];

// If it's running on Sherlo server set Storybook mode
NSString *configPath = [sherloDirectoryPath stringByAppendingPathComponent:CONFIG_FILENAME];
BOOL doesSherloConfigFileExist = [[NSFileManager defaultManager] fileExistsAtPath:configPath isDirectory:NO];
if (doesSherloConfigFileExist) {
appOrStorybookMode = @"storybook";
}
}
return self;
}

- (dispatch_queue_t)methodQueue
{
return RCTGetUIManagerQueue();
Expand All @@ -30,11 +50,14 @@ + (BOOL)requiresMainQueueSetup
resolve(appOrStorybookMode);
}

RCT_EXPORT_METHOD(setAppOrStorybookMode:(NSString *)mode
RCT_EXPORT_METHOD(setAppOrStorybookModeAndRestart:(NSString *)mode
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
appOrStorybookMode = mode;

// Restart JS
[_bridge reload];
resolve(nil);
}

Expand Down Expand Up @@ -201,7 +224,8 @@ - (void)reject:(RCTPromiseRejectBlock)reject withError:(NSError *)error
- (NSDictionary *)constantsToExport
{
return @{
@"RNDocumentDirectoryPath": [self getPathForDirectory:NSDocumentDirectory],
@"sherloDirectoryPath": sherloDirectoryPath,
@"appOrStorybookMode": appOrStorybookMode,
};
}

Expand Down
114 changes: 43 additions & 71 deletions packages/react-native-storybook/src/getAppWithStorybook.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactElement, useEffect, useState } from 'react';
import { DevSettings } from 'react-native';
import { isSherloServer, SherloModule } from './helpers';
import { isExpoGo, SherloModule } from './helpers';
import { passSetModeToOpenStorybook } from './openStorybook';
import { AppOrStorybookMode } from './types';

Expand All @@ -15,91 +15,63 @@ function getAppWithStorybook({
}: {
App: () => ReactElement;
Storybook: () => ReactElement;
}): () => ReactElement | null {
}): () => ReactElement {
return () => {
const [visibleMode, setVisibleMode] = useState<AppOrStorybookMode | null>('app');
const [pendingMode, setPendingMode] = useState<AppOrStorybookMode | null>(null);

const setMode = (mode: AppOrStorybookMode | 'toggle') => {
setVisibleMode((prevMode) => {
let newMode: AppOrStorybookMode;
if (mode === 'toggle') {
newMode = prevMode === 'app' ? 'storybook' : 'app';
} else {
newMode = mode;
}
/**
* Depending on `appOrStorybookMode`, the following component is rendered:
* - by default, returns the App
* - returns Storybook if running on Sherlo servers
* - on reload, returns last selected mode: Storybook if the developer was
* working on it, or App otherwise
*/
const { appOrStorybookMode } = SherloModule;

const [expoGoMode, setExpoGoMode] = useState<AppOrStorybookMode | null>(null);

const setMode = async (mode: AppOrStorybookMode | 'toggle') => {
let newMode: AppOrStorybookMode;
if (mode === 'toggle') {
newMode = appOrStorybookMode === 'app' ? 'storybook' : 'app';
} else {
newMode = mode;
}

if (newMode === prevMode) {
/**
* Simply return previous mode if it didn't change
*/

return prevMode;
} else {
/**
* If the mode has changed, temporarily set visibleMode to `null` and
* set the new mode as pending. This ensures a one cycle pause to
* avoid rendering issues on Android emulators during the transition
* between `app` and `storybook` modes.
*/

SherloModule.setAppOrStorybookMode(newMode); // Persist the new mode for future app restarts
setPendingMode(newMode); // Set the new mode as pending
return null; // Temporarily set visibleMode to null
}
});
};
if (!isExpoGo) {
/**
* This approach ensures that the last selected mode (App or Storybook)
* is remembered and restored after a reload. Additionally, it resolves
* rendering issues when switching between App and Storybook by
* restarting the app during mode changes.
*/

useEffect(() => {
if (pendingMode !== null) {
if (newMode === appOrStorybookMode) return;

SherloModule.setAppOrStorybookModeAndRestart(newMode);
} else {
/**
* If pendingMode is defined, set it as the visible mode.
* At this point, we know the render cycle has completed rendering
* `null`, so we can safely set the target mode, preventing rendering
* issues on Android emulators.
* For Expo Go, use a simpler method due to lack of native module access
*/

setVisibleMode(pendingMode);
setPendingMode(null);
if (newMode === expoGoMode) return;

setExpoGoMode(newMode);
}
}, [pendingMode]);
};

useEffect(() => {
(async () => {
if (await isSherloServer()) {
/**
* If the app is running on a Sherlo server, switch to Storybook
* mode for testing
*/

setMode('storybook');
} else if (__DEV__) {
/**
* If the app is running in development mode, retrieve the last
* selected mode if the app was reset, otherwise default to the App
*/

const selectedModeBeforeReset = await SherloModule.getAppOrStorybookMode();
setMode(selectedModeBeforeReset ?? 'app');

if (__DEV__) {
addToggleStorybookToDevMenu(setMode);
} else {
/**
* If the app is running in production mode, display the App
*/

setMode('app');
}
})();
}, []);

useEffect(() => {
passSetModeToOpenStorybook(setMode);
passSetModeToOpenStorybook(setMode);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (visibleMode === null) return null;

if (visibleMode === 'storybook') return <Storybook />;
if (appOrStorybookMode === 'storybook' || expoGoMode === 'storybook') {
return <Storybook />;
}

return <App />;
};
Expand Down
Loading

0 comments on commit 150fb2c

Please sign in to comment.