Skip to content

Commit

Permalink
Improve React Native experience (#1462)
Browse files Browse the repository at this point in the history
* Improve React Native guide

* Update reactNative.md

* Update reactNative.md

* Update reactNative.md

* Add remote renderer overlay

* Improve remote renderer connected overlay

* Fix typo and improve brevity
  • Loading branch information
ovidiuch committed Apr 26, 2023
1 parent 04f1bc7 commit b216096
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 56 deletions.
45 changes: 37 additions & 8 deletions docs/reactNative.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,25 @@ At this point Cosmos should successfully read your fixtures. One more step befor

This is very similar to a [custom bundler setup](customBundlerSetup.md). Cosmos cannot plug itself automatically into React Native's build pipeline (Metro), but you can do it with minimal effort.

Replace your `App.js` entrypoint with the following code:
Here's a basic file structure to get going. You can tweak this after everything's working.

1. Your production app entry point: `App.main.js`.
2. Your Cosmos renderer entry point: `App.cosmos.js`.
3. The root entry point that decides which to load: `App.js`.

> If you're using TypeScript replace `.js` file extensions with `.tsx`.
First, rename your existing `App.js` to `App.main.js`.

Then add the Cosmos renderer under `App.cosmos.js`:

```jsx
// App.js
// App.cosmos.js
import React, { Component } from 'react';
import { NativeFixtureLoader } from 'react-cosmos-native';
import { rendererConfig, moduleWrappers } from './cosmos.userdeps.js';

export default class App extends Component {
export default class CosmosApp extends Component {
render() {
return (
<NativeFixtureLoader
Expand All @@ -114,7 +124,17 @@ export default class App extends Component {
}
```

This is a temporary solution to get going with Cosmos. Once you see your fixtures rendering properly you'll probably want to split your App entry point to load Cosmos in development and your root component in production. Something like this:
> When using TypeScript you'll notice an error related to `cosmos.userdeps.js`, which is a plain JS module. We're working on providing an option to generate `cosmos.userdeps.ts` soon. Meanwhile you can ignore this error by slapping a naughty `@ts-ignore` comment:
>
> ```diff
> <NativeFixtureLoader
> rendererConfig={rendererConfig}
> + // @ts-ignore
> moduleWrappers={moduleWrappers}
> />
> ```
Finally, create a new `App.js` that routes between your main and Cosmos entry points based on enviromnent:
```js
// App.js
Expand All @@ -123,16 +143,25 @@ module.exports = global.__DEV__
: require('./App.main');
```
Where `App.cosmos.js` contains the code above that renders `NativeFixtureLoader` and `App.main.js` contains your original App.js.

6\. **Render fixture in simulator**
That's it!

That's it. Open your app in the simulator and the Cosmos renderer should say "No fixture selected". Go back to your React Cosmos UI, click on the `Hello` fixture and it will render in the simulator.
Open your app in the simulator and the Cosmos renderer should say "No fixture selected". Go back to your React Cosmos UI, click on the `Hello` fixture and it will render in the simulator.

**Congratulations 😎**

You've taken the first step towards designing reusable components. You're ready to prototype, test and interate on components in isolation.

## App fixture

You'll often want to load the entire app in development. The simplest way to do this without disconnecting the Cosmos entry point is to create an App fixture:

```jsx
// App.fixture.js
import App from './App.main';

export default () => <App />;
```

## Initial fixture

You can configure the Cosmos Native renderer to auto load a fixture on init.
Expand Down
11 changes: 7 additions & 4 deletions packages/react-cosmos-ui/src/components/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import React from 'react';
import { BaseSvg, SvgChildren } from './BaseSvg.js';

type IconProps = {
children: SvgChildren;
export type IconProps = {
size?: number | string;
strokeWidth?: number;
};

export function Icon({ children, size = '100%' }: IconProps) {
type Props = IconProps & {
children: SvgChildren;
};
export function Icon({ children, size = '100%', strokeWidth = 1.5 }: Props) {
return (
<BaseSvg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
>
Expand Down
13 changes: 4 additions & 9 deletions packages/react-cosmos-ui/src/components/icons/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import React from 'react';
import { Icon } from '../Icon.js';

// Add common interface to each icon when needed
type Props = {
size?: number;
};
import { Icon, IconProps } from '../Icon.js';

export const ChevronLeftIcon = () => (
<Icon>
Expand Down Expand Up @@ -75,7 +70,7 @@ export const RefreshCwIcon = () => (
</Icon>
);

export const RefreshCcwIcon = (props: Props) => (
export const RefreshCcwIcon = (props: IconProps) => (
<Icon {...props}>
<polyline points="1 4 1 10 7 10"></polyline>
<polyline points="23 20 23 14 17 14"></polyline>
Expand Down Expand Up @@ -104,8 +99,8 @@ export const EditIcon = () => (
</Icon>
);

export const CheckCircleIcon = () => (
<Icon>
export const CheckCircleIcon = (props: IconProps) => (
<Icon {...props}>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</Icon>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import styled from 'styled-components';
import { CheckCircleIcon } from '../../../components/icons/index.js';
import { grey144 } from '../../../style/colors.js';
import {
RendererOverlayContainer,
RendererOverlayIconWrapper,
RendererOverlayMessage,
} from './rendererOverlayShared.js';

export function RemoteRendererConnected() {
return (
<RendererOverlayContainer>
<RendererOverlayIconWrapper>
<IconContainer>{<CheckCircleIcon strokeWidth={0.55} />}</IconContainer>
</RendererOverlayIconWrapper>
<RendererOverlayMessage>Remote renderer connected</RendererOverlayMessage>
</RendererOverlayContainer>
);
}

const svgRingRatio = 26.667 / 32;
const ringSize = 34;
const iconSize = ringSize / svgRingRatio;

const IconContainer = styled.span`
width: ${iconSize}px;
height: ${iconSize}px;
color: ${grey144};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import retry from '@skidding/async-retry';
import { render } from '@testing-library/react';
import React from 'react';
import { wrapActSetTimeout } from '../../../testHelpers/wrapActSetTimeout.js';
import { RemoteRendererOverlay } from './RemoteRendererOverlay.js';

it('does not immediately render anything before renderer is connected', () => {
const { container } = render(
<RemoteRendererOverlay rendererConnected={false} />
);
expect(container).toMatchInlineSnapshot(`<div />`);
});

it('renders "waiting for renderer" state after waiting for some time', async () => {
wrapActSetTimeout();
const { getByText } = render(
<RemoteRendererOverlay rendererConnected={false} />
);
await retry(() => getByText(/waiting for renderer/i));
});

it('renders "remote renderer connected" after renderer is connected', () => {
const { getByText } = render(
<RemoteRendererOverlay rendererConnected={true} />
);
getByText(/remote renderer connected/i);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { RemoteRendererConnected } from './RemoteRendererConnected.js';
import { WaitingForRenderer } from './WaitingForRenderer.js';

type Props = {
rendererConnected: boolean;
};
export function RemoteRendererOverlay({ rendererConnected }: Props) {
return rendererConnected ? (
<RemoteRendererConnected />
) : (
<WaitingForRenderer />
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { WaitingForRenderer } from './WaitingForRenderer.js';
type Props = {
runtimeStatus: RuntimeStatus;
};

export function RendererOverlay({ runtimeStatus }: Props) {
if (runtimeStatus === 'pending') {
return <WaitingForRenderer />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
import React from 'react';
import styled from 'styled-components';
import { DelayedLoading } from '../../../components/DelayedLoading.js';
import { createGreyColor, grey144, grey192 } from '../../../style/colors.js';
import { grey144 } from '../../../style/colors.js';
import {
RendererOverlayContainer,
RendererOverlayIconWrapper,
RendererOverlayMessage,
} from './rendererOverlayShared.js';

export function WaitingForRenderer() {
return (
<DelayedLoading delay={500}>
<Container>
<Loader />
<Message>Waiting for renderer...</Message>
</Container>
<RendererOverlayContainer>
<RendererOverlayIconWrapper>
<Loader />
</RendererOverlayIconWrapper>
<RendererOverlayMessage>Waiting for renderer...</RendererOverlayMessage>
</RendererOverlayContainer>
</DelayedLoading>
);
}

const Container = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${createGreyColor(8, 0.85)};
border-radius: 3px;
padding: 16px 24px;
display: flex;
flex-direction: column;
align-items: center;
`;

// Copied from https://codepen.io/bernethe/pen/dorozd
const Loader = styled.div`
margin: 12px 0 24px 0;
width: 32px;
height: 32px;
width: 34px;
height: 34px;
border-radius: 50%;
position: relative;
:before,
:after {
content: '';
box-sizing: border-box;
border: 1px ${grey144} solid;
border-radius: 50%;
width: 100%;
Expand Down Expand Up @@ -80,9 +74,3 @@ const Loader = styled.div`
}
}
`;

const Message = styled.p`
color: ${grey192};
text-transform: uppercase;
white-space: nowrap;
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react';
import { RemoteRendererConnected } from './RemoteRendererConnected.js';
import { WaitingForRenderer } from './WaitingForRenderer.js';

export default {
'waiting for renderer': <WaitingForRenderer />,

'remote renderer connected': <RemoteRendererConnected />,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import styled from 'styled-components';
import { createGreyColor, grey192 } from '../../../style/colors.js';

export const RendererOverlayContainer = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${createGreyColor(8, 0.9)};
border-radius: 3px;
height: 116px;
padding: 0 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
`;

export const RendererOverlayIconWrapper = styled.div`
height: 76px;
display: flex;
align-items: center;
`;

export const RendererOverlayMessage = styled.p`
margin-bottom: 16px;
color: ${grey192};
text-transform: uppercase;
white-space: nowrap;
`;
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import React from 'react';
import { Slot } from 'react-plugin';
import styled from 'styled-components';
import { RemoteRendererOverlay } from './RendererOverlay/RemoteRendererOverlay.js';
import { RendererOverlay } from './RendererOverlay/RendererOverlay.js';
import { WaitingForRenderer } from './RendererOverlay/WaitingForRenderer.js';
import { RuntimeStatus } from './spec.js';

export type OnIframeRef = (elRef: null | HTMLIFrameElement) => void;

type Props = {
rendererUrl: null | string;
rendererConnected: boolean;
runtimeStatus: RuntimeStatus;
onIframeRef: OnIframeRef;
};

export const RendererPreview = React.memo(function RendererPreview({
rendererUrl,
rendererConnected,
runtimeStatus,
onIframeRef,
}: Props) {
if (!rendererUrl) {
// This code path is used when Cosmos is in React Native mode
return (
<Container>
{runtimeStatus === 'pending' && <WaitingForRenderer />}
</Container>
);
return <RemoteRendererOverlay rendererConnected={rendererConnected} />;
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ plug('rendererPreview', ({ pluginContext }) => {
return (
<RendererPreview
rendererUrl={getRendererUrl(pluginContext)}
rendererConnected={getRendererConnected(pluginContext)}
runtimeStatus={pluginContext.getState().runtimeStatus}
onIframeRef={handleIframeRef}
/>
Expand All @@ -59,3 +60,7 @@ function getRuntimeStatus({ getState }: RendererPreviewContext) {
function getRendererUrl({ getMethodsOf }: RendererPreviewContext) {
return getMethodsOf<CoreSpec>('core').getWebRendererUrl();
}

function getRendererConnected({ getMethodsOf }: RendererPreviewContext) {
return getMethodsOf<RendererCoreSpec>('rendererCore').isRendererConnected();
}

0 comments on commit b216096

Please sign in to comment.