necolas / react-native-web

Cross-platform React UI packages
https://necolas.github.io/react-native-web
MIT License
21.61k stars 1.79k forks source link

DeviceEventEmitter supported on web #1115

Closed xiaohuoni closed 5 years ago

xiaohuoni commented 6 years ago

DeviceEventEmitter supported on web. How to use DeviceEventEmitter in web is just a emit event between pages.

Is your feature request related to a problem? Please describe. When I transferred an old RN project to web, I used the DeviceEventEmitter in the RN project, so I reported the wrong https://github.com/necolas/react-native-web/issues/841

TypeError: Cannot read property 'addListener' of undefined

Describe a solution you'd like Use DeviceEventEmitter.emit in Web

Describe alternatives you've considered eventemitter3 Additional context

necolas commented 6 years ago

What are you expecting from this module on web? Please can you fill out the issue template as required, then I'll reopen this.

xiaohuoni commented 6 years ago

@necolas look this!

necolas commented 6 years ago

Thank you.

Please can you include an example of the code you have that is causing the error? Is it in a dependency or your own code? What events are you listening to and what do you do after the events are emitted?

xiaohuoni commented 6 years ago

Core code

export default class ModalTarget extends Component {
  static add(modal) {
    const modalKey = ++keyValue;
    DeviceEventEmitter.emit('addModals', { modalKey, modal });
    return modalKey;
  }

  constructor(props) {
    super(props);
    this.state = {
      modals: [],
    };
  }

  componentWillMount() {
    DeviceEventEmitter.addListener('addModals', event => this.add(event));
  }

  componentWillUnmount() {
    DeviceEventEmitter.removeAllListeners('addModals');
  }

  add(event) {
    const { modals } = this.state;
    modals.push(event);
    this.setState({ modals });
  }

  render() {
    const { modals } = this.state;
    return (
      <View style={{ backgroundColor: Theme.fill_grey, flex: 1 }}>
        {this.props.children}
        {modals.map(item => (
          <View key={`modals${item.key}`} >
            {item.modal}
          </View>
        ))}
      </View>
    );
  }
}

i can use ModalTarget.add demo :https://github.com/xiaohuoni/whale-rn/

xiaohuoni commented 6 years ago

@necolas Hello,Do you have any development plan? Or do you have any plans to consider?

necolas commented 6 years ago

Yes I'll review a PR for this.

This article says it's best to use NativeEventEmitter so that needs to be added too https://levelup.gitconnected.com/react-native-events-in-gory-details-what-happens-on-the-way-to-listeners-2cee6c55940c

xiaohuoni commented 6 years ago

@necolas

import events from "events";
const { EventEmitter } = events;
const DeviceEventEmitter = new EventEmitter();

export default DeviceEventEmitter;

I use the code in my project.It works very well. Because I think on web, it only needs js2js.

geminiyellow commented 5 years ago

oh, @necolas , want to use react-native-modal but it raise:

index.js:1 Uncaught TypeError: Cannot read property 'addListener' of undefined

yes, DeviceEventEmitter not here

Shivani12345 commented 5 years ago

how to solve this error undefined is not an object(evaluating 'subscription.listener.appy') ?

EvanBacon commented 5 years ago

This article says it's best to use NativeEventEmitter so that needs to be added too...

AFAIK Turbo modules will change a lot of emitter functionality works so it may not make sense to start supporting it now.

laszhiai commented 5 years ago

I have the same problem when i use react-native-web on RN project. When i new NativeEventEmitter(), it will throw an error on platform ios. Can solve this problem

shirakaba commented 5 years ago

Thanks for that work Evan, that's a real help!

Here's how I'm consuming react-native-modal (which consumes NativeEventEmitter) in a project created using a boilerplate for React Native Web based on create-react-app (not Expo Web).

Installing the fork of React Native Web that re-exports DeviceEventEmitter

I installed Evan Bacon's fork of React Native Web that implements a PR for re-exporting DeviceEventEmitter. This PR has been merged into Expo for Web already, but has not yet been merged into React Native Web:

git clone https://github.com/EvanBacon/react-native-web.git
cd react-native-web
git checkout "@evanbacon/plugins/DeviceEventEmitter"
yarn install
yarn compile
cd packages/react-native-web
pwd
# This is the filepath to the React Native Web package under the React Native Web monorepo (GitHub project)
# Now in your RNW project, run `yarn remove react-native-web && yarn add <that filepath>`

Customising a Create React App project to support NativeEventEmitter

I examined Expo for Web's webpack config, and adapted my Create React App's webpack config to incorporate the necessary changes to support NativeEventEmitter (in addition to the usual react-native-web babel plugin).

I customised the Create React App webpack config using rescripts (hence the .rescriptsrc.js file), but Customize React App would also work.

// .rescriptsrc.js
const path = require('path');
const { getPaths, edit, getWebpackPlugin, paths } = require('@rescripts/utilities');

module.exports = config => {
    const isEnvDevelopment = config.mode === "development";
    const isEnvProduction = config.mode === "production";

    config.resolve.alias = {
        ...config.resolve.alias,

        /* https://github.com/expo/expo-cli/blob/72e35aea644ce38a5389595f3ff18b45454d7986/packages/webpack-config/webpack/webpack.common.js#L312-L322 */
        // Alias internal react-native modules to react-native-web
        'react-native/Libraries/Components/View/ViewStylePropTypes$':
            'react-native-web/dist/exports/View/ViewStylePropTypes',
        'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter$':
            'react-native-web/dist/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter',
        'react-native/Libraries/vendor/emitter/EventEmitter$':
            'react-native-web/dist/vendor/react-native/emitter/EventEmitter',
        'react-native/Libraries/vendor/emitter/EventSubscriptionVendor$':
            'react-native-web/dist/vendor/react-native/emitter/EventSubscriptionVendor',
        'react-native/Libraries/EventEmitter/NativeEventEmitter$':
            'react-native-web/dist/vendor/react-native/NativeEventEmitter',
    };

    /* With reference to: https://github.com/arackaf/customize-cra/blob/master/src/utilities.js */
    const babelLoaderForSrcPaths = getPaths(
        inQuestion => {
            if(inQuestion && inQuestion.loader && inQuestion.loader.includes('babel-loader')){
                /* This distinguishes the two babel-loaders: here we select the one for src rather than node_modules. */
                return inQuestion.include === paths.src; /* Where paths.src is 'our' reference to paths.appSrc. */
            }
            return false;
        },
        config
    );

    /* With reference to: https://github.com/arackaf/customize-cra/blob/master/api.md#babelinclude */
    config = edit(
        (babelLoaderForSrc) => {
            babelLoaderForSrc.include = [
                // make sure you link your own source
                path.resolve("src"),

                // From react-native-modal
                path.resolve("node_modules/react-native-modal"),
                path.resolve("node_modules/react-native-animatable"),
            ];

            babelLoaderForSrc.options.plugins.push(
                ["react-native-web", { commonjs: true }],
            );
            return babelLoaderForSrc;
        },
        babelLoaderForSrcPaths,
        config
    );

    /* The CRA config defines process.env with an extra layer of quotes on values, and so so shall we.
     * This substitutes for the global variables normally injected by the Metro bundler:
     *   https://github.com/facebook/metro/blob/1f9a557e892716dffe9ffa250e0c62c2a65a0bdb/packages/metro/src/lib/getPreludeCode.js#L22
     * I had expected metro-react-native-babel-preset to do this for us, but I guess it doesn't.
     * DefinePlugin docs: https://webpack.js.org/plugins/define-plugin/ */
    const definePlugin = getWebpackPlugin("DefinePlugin", config);
    Object.assign(
        definePlugin.definitions,
        {
            /* Just because it's defined globally doesn't mean that window links to it, so we define both here (it's not redundant).
             * However, technically no React code references window.__DEV__ in practice; we shouldn't really look for it on window ourselves either. */
            __DEV__: `${isEnvDevelopment}`,
            "window.__DEV__": `${isEnvDevelopment}`,
        }
    );

    return config;
};

Disclaimer

This may not be totally ready for use, as Evan notes in https://github.com/necolas/react-native-web/pull/1402 that there are still to-dos to complete. I don't know. And either way, DeviceEventEmitter will be changing due to TurboModules, so this method may soon become irrelevant. But if you need something working right now, here's an option.

necolas commented 5 years ago

8f5e7d4e1468abe028cfab339e275097ceb75d3e

shirakaba commented 5 years ago

An update to my previous advice: no need to install Evan Bacon's fork for this feature now, as it's been merged into the core. Just installing "react-native-web": "0.11.6" suffices (my Webpack instructions still apply).