GithubHelp home page GithubHelp logo

react-side-effect's Introduction

React Side Effect Downloads npm version

Create components whose prop changes map to a global side effect.

Installation

npm install --save react-side-effect

As a script tag

Development

<script src="https://unpkg.com/react/umd/react.development.js" type="text/javascript"></script>
<script src="https://unpkg.com/react-side-effect/lib/index.umd.js" type="text/javascript"></script>

Production

<script src="https://unpkg.com/react/umd/react.production.min.js" type="text/javascript"></script>
<script src="https://unpkg.com/react-side-effect/lib/index.umd.min.js" type="text/javascript"></script>

Use Cases

  • Setting document.body.style.margin or background color depending on current screen;
  • Firing Flux actions using declarative API depending on current screen;
  • Some crazy stuff I haven't thought about.

How's That Different from componentDidUpdate?

It gathers current props across the whole tree before passing them to side effect. For example, this allows you to create <BodyStyle style> component like this:

// RootComponent.js
return (
  <BodyStyle style={{ backgroundColor: 'red' }}>
    {this.state.something ? <SomeComponent /> : <OtherComponent />}
  </BodyStyle>
);

// SomeComponent.js
return (
  <BodyStyle style={{ backgroundColor: this.state.color }}>
    <div>Choose color: <input valueLink={this.linkState('color')} /></div>
  </BodyStyle>
);

and let the effect handler merge style from different level of nesting with innermost winning:

import { Component, Children } from 'react';
import PropTypes from 'prop-types';
import withSideEffect from 'react-side-effect';

class BodyStyle extends Component {
  render() {
    return Children.only(this.props.children);
  }
}

BodyStyle.propTypes = {
  style: PropTypes.object.isRequired
};

function reducePropsToState(propsList) {
  var style = {};
  propsList.forEach(function (props) {
    Object.assign(style, props.style);
  });
  return style;
}

function handleStateChangeOnClient(style) {
  Object.assign(document.body.style, style);
}

export default withSideEffect(
  reducePropsToState,
  handleStateChangeOnClient
)(BodyStyle);

On the server, you’ll be able to call BodyStyle.peek() to get the current state, and BodyStyle.rewind() to reset for each next request. The handleStateChangeOnClient will only be called on the client.

API

withSideEffect: (reducePropsToState, handleStateChangeOnClient, [mapStateOnServer]) -> ReactComponent -> ReactComponent

A higher-order component that, when mounting, unmounting or receiving new props, calls reducePropsToState with props of each mounted instance. It is up to you to return some state aggregated from these props.

On the client, every time the returned component is (un)mounted or its props change, reducePropsToState will be called, and the recalculated state will be passed to handleStateChangeOnClient where you may use it to trigger a side effect.

On the server, handleStateChangeOnClient will not be called. You will still be able to call the static rewind() method on the returned component class to retrieve the current state after a renderToString() call. If you forget to call rewind() right after renderToString(), the internal instance stack will keep growing, resulting in a memory leak and incorrect information. You must call rewind() after every renderToString() call on the server.

For testing, you may use a static peek() method available on the returned component. It lets you get the current state without resetting the mounted instance stack. Don’t use it for anything other than testing.

Usage

Here's how to implement React Document Title (both client and server side) using React Side Effect:

import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
import withSideEffect from 'react-side-effect';

class DocumentTitle extends Component {
  render() {
    if (this.props.children) {
      return Children.only(this.props.children);
    } else {
      return null;
    }
  }
}

DocumentTitle.propTypes = {
  title: PropTypes.string.isRequired
};

function reducePropsToState(propsList) {
  var innermostProps = propsList[propsList.length - 1];
  if (innermostProps) {
    return innermostProps.title;
  }
}

function handleStateChangeOnClient(title) {
  document.title = title || '';
}

export default withSideEffect(
  reducePropsToState,
  handleStateChangeOnClient
)(DocumentTitle);

react-side-effect's People

Contributors

ambar avatar browniebroke avatar davidfurlong avatar dependabot[bot] avatar dimitrinicolas avatar gaearon avatar gannoncurran avatar glenjamin avatar joshfry avatar karlhorky avatar lo1tuma avatar lourd avatar realityking avatar taou-maimai avatar vond avatar wincent avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-side-effect's Issues

Flow warnings

I don't know why, by from zilions Node modules I have in Este, only this library produces Flow warnings. Probably something with fbjs.

screen shot 2016-08-10 at 03 58 26

Warning with React 0.14.0-rc1

Getting the following console warning when using react-document-title v2.0.0 and react-side-effect v1.0.1:

SideEffect(Component)(...): React component classes must extend React.Component.

Tab Navigation + Different Tab Stack Lengths

Hi,
I'm trying to use this wonderful library to implement the Android Back Button functionality mentioned in this blog article.

Problem is, my use case is a little different, it can't automatically use the "last mounted" component I use for catching the back button event, it needs to use the one of the currently focused screen.


For example, this works:

  1. Tab A: Tab A Root
  2. Tab B: Tab B Root > Screen B <- focused, back button here, listener of Screen B called correcty
  3. Tab C: Tab C Root

While this is not working:

  1. Tab A: Tab A Root
  2. Tab B: Tab B Root > Screen B <-- focused, back button here, listener of Subscreen C is called
  3. Tab C: Tab C Root> Screen C > Subscreen C

Any ideas how this scenario could be handled? Thanks!

React 18.2.0 peer-dependency problem with [email protected]

EDIT: Sorry after making this issue, I realized the issue probably needs to be fixed in react-helmet.
Feel free to close this issue if it's irrelevant to react-side-effect.

$ npm i
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: [email protected]
npm WARN Found: [email protected]
npm WARN node_modules/react
npm WARN   react@"^18.2.0" from the root project
npm WARN   5 more (react-dom, react-flatpickr, react-helmet, react-router, react-router-dom)
npm WARN 
npm WARN Could not resolve dependency:
npm WARN peer react@"^16.3.0 || ^17.0.0" from [email protected]
npm WARN node_modules/react-helmet/node_modules/react-side-effect
npm WARN   react-side-effect@"^2.1.0" from [email protected]
npm WARN   node_modules/react-helmet
npm WARN 
npm WARN Conflicting peer dependency: [email protected]
npm WARN node_modules/react
npm WARN   peer react@"^16.3.0 || ^17.0.0" from [email protected]
npm WARN   node_modules/react-helmet/node_modules/react-side-effect
npm WARN     react-side-effect@"^2.1.0" from [email protected]
npm WARN     node_modules/react-helmet
$ npm list --depth=99 | grep react
├─┬ [email protected]
│ ├── [email protected] deduped
├─┬ [email protected]
│ │ └── [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ ├── [email protected]
│ ├─┬ [email protected]
│ │ └── [email protected] deduped invalid: "^16.3.0 || ^17.0.0" from node_modules/react-helmet/node_modules/react-side-effect
│ └── [email protected] deduped
├─┬ [email protected]
│ ├── [email protected] deduped
│ ├── [email protected] deduped
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
└─┬ [email protected]

Problems in installation

after installing this library I'm not able to run react-native start and get this error:
(I'm installing it in a react native project)
`module.js:472
throw err;
^

Error: Cannot find module 'anymatch'
at Function.Module._resolveFilename (module.js:470:15)
at Function.Module._load (module.js:418:25)
at Module.require (module.js:498:17)
at require (internal/module.js:20:19)
at Object. (/home/sarahm/dev/closapp2/node_modules/jest-haste-map/node_modules/sane/src/common.js:3:16)
at Module._compile (module.js:571:32)
at Module._extensions..js (module.js:580:10)
at Object.require.extensions.(anonymous function) [as .js] (/home/sarahm/dev/closapp2/node_modules/babel-register/lib/node.js:152:7)
at Module.load (module.js:488:32)
at tryModuleLoad (module.js:447:12)`

Can’t find instance of wrapped side-effect component with react test utils

I’ve tried to update to react-side-effect 1.0.1 today, but my tests are failing.

I have a component MetaDescription which is wrapped using the withSideEffect function.
In my test I render the Application component which renders MetaDescription.

var application = TestUtils.renderIntoDocument(<Application />);
var description = TestUtils.findRenderedComponentWithType(application, MetaDescription);

expect(description).to.have.deep.property('props.description', 'foo bar');

findRenderedComponentWithType fails, because it can’t find an instance of MetaDescription:

    Error: Did not find exactly one match for componentType:function SideEffect() {
      Component.apply(this, arguments);
    }

I’ve change react-side-effect locally to use React.createClass instead of only calling Component.apply(this, arguments); in the constructor, and everything worked again.

[handleStateChangeOnClient] pass previous state

I know it can be stored with decorated component, but it would be good to have.

Use case

import $ from 'jquery';
import cx from 'classnames';
import { Component, Children, PropTypes } from 'react';
import withSideEffect from 'react-side-effect';

class BodyClassName extends Component {
  render() {
    return Children.only(this.props.children);
  }
}

BodyClassName.propTypes = {
  className: PropTypes.string
};

function reducePropsToState(propsList) {
  return propsList.reduce(function(out, props) {
    return cx(out, props.className);
  }, '');
}

function handleStateChangeOnClient(classNames, prevClassNames) {
  $('body').removeClass(prevClassNames).addClass(classNames);
}

export default withSideEffect(
  reducePropsToState,
  handleStateChangeOnClient
)(BodyStyle);

Output isn't always based on nesting order

I noticed that the documentation here and over at Helmet seems to imply that props get merged based on nesting order, but that's not always the case. It's based on mounting order, which doesn't always === nesting order.

During the lifetime of the app any component can be mounted at any point in the tree, but it gets treated as if it's the innermost component regardless. This is a problem in react-document-title for example since it assumes that the last component in the list is the inner-most one (https://github.com/gaearon/react-document-title/blob/master/index.js#L7)

Suggested improvement on shouldComponentUpdate

If I have a component that I am passing into SideEffect that has an API as follows:

<Component
     data={[
          {key: "value"},
          {key: "value"}
     ]}
/>

The problem is that when an array of objects is passed in as a prop, SideEffect will do a shallow compare that will always consider these props different because React considers this to be a new array and objects created. Would you consider doing a deep equal compare in shouldComponentUpdate in order to prevent unnecessary renders and emitChange calls?

We found deep-equal (https://github.com/substack/node-deep-equal) is a good library.

Thanks for your consideration.

React 0.14 support

Since 0.14 version, React does not contain 'react/lib/invariant' and 'react/lib/shallowEqual' files. Now these files are available in facebook/fbjs repository.

Expected handleStateChangeOnClient to be a function

I have created this component for adding and removing css classes on the document body, taking the docs as a lead, but it is giving me the title error message. What am I missing?

I'm using v1.0.1

import { Component, Children, PropTypes } from 'react'
import withSideEffect from 'react-side-effect'

class BodyClasses extends Component {
  static propTypes = {
    add: PropTypes.string,
    remove: PropTypes.string
  }

  render() {
    return Children.only(this.props.children)
  }
}

function reducePropsToState (propsList) {
  var diff = {add: [], remove: []}
  propsList.forEach(function (props) {
    if (props.add) diff.add.push(props.add)
    if (props.remove) diff.remove.push(props.remove)
  })
  return diff
}

function handleStateChangeOnClient (diff) {
  diff.add.forEach(className => document.body.classList.add(className))
  diff.remove.forEach(className => document.body.classList.remove(className))
}

export default withSideEffect(
  reducePropsToState,
  handleStateChangeOnClient
)(BodyClasses)

Exposing length of mountedInstances to properly hydrate the client

I'm attempting to fix this Helmet issue (nfl/react-helmet#149) and had the idea of making the client wait until all instances are mounted before allowing handleStateChangeOnClient to change the DOM. Then on the first render of the client (after SSR), it would only do one client state change which would match the server rendered side effects and thus would leave all the side effects intact. In order to "hydrate" the client like this, I'd need to pass along the number of mounted instances so that the client knows how many components to wait for. I can certainly add a mountedInstances inside of Helmet, but I felt it would be better to perhaps expose this from react-side-effect instead and not duplicate functionality.

I can certainly offer a PR here as well. Please let me know. Thanks!

Integration with Fluxible

How can my side effect execute an FluxibleAction? Since there is no bind to context, I couldn't achieve this. In my case, I'm doing a component called TitledScreen which could be used at any point of my component tree and TitledScreen should join the given titles in the right order and them execute an FluxibleAction, but onChange has no context.

MS Edge: Click handlers aren't called when using SideEffect

In a recent project, I discovered some weird behaviour in MS Edge when using SideEffect in combination with nested Flexbox containers and existing children in the React mount container: click handlers in the mounted components aren't called.

The issue only surfaces in MS Edge 12 and 13. In IE 11, Chrome, Firefox and Safari the issue is not reproducible.

The following code uses React Document Title, but I was able to reproduce the issue with React Helmet as well. That makes me think that React Side Effect might be the influential factor here.

Code to reproduce:

<!-- Both `.wrapper` and `.root` are Flex Containers (`display: flex`) -->
<div class="wrapper">
    <!-- This `#root` div will be used as the React mount container -->
    <div id="root" class="root">
        <!-- Note the use of existing children in the mount container -->
        <p>loading...</p>
    </div>
</div>
import React from 'react';
import ReactDOM from 'react-dom';
import DocumentTitle from 'react-document-title';

const App = React.createClass({
    render() {

        /**
         * The click handler on the button isn't called in MS Edge.
         *
         * When the `<DocumentTitle />` is removed, the click handler is called.
         */
        return (
            <div>
                <DocumentTitle title="My Title" />
                <button onClick={() => console.log('clicked')}>Click me</button>
            </div>
        );
    }
});

// Render into the nested Flex Container
ReactDOM.render(
    <App />,
    document.getElementById('root')
);

I've put together a repo with the smallest reproducible test case I could create: https://github.com/leonderijke/react-edge-bug

I'm not sure if this is a bug in SideEffect, React or MS Edge, I thought filing an issue here might be a proper first step.

1.1.4 is broken

Your patch release changed the module exports. react-document-title is now broken

×
TypeError: withSideEffect is not a function
./node_modules/react-document-title/index.js
node_modules/react-document-title/index.js:37
  34 |   }
  35 | };
  36 | 
> 37 | module.exports = withSideEffect(
  38 |   reducePropsToState,
  39 |   handleStateChangeOnClient
  40 | )(DocumentTitle);

question - why `rewind` is not supported at client side?

I am currently working with Apollo GraphQL and I want to do some sort of prefetch before transiting to a new route at client side. This would need to walk through the whole component tree to fetch data before the actual rendering happens. But in my component tree, there are some component produces side effect where I have to do rewind after going through the component tree. But the rewind method is only callable at server side.

I am wondering the reason of this nature

Receiving React 15 propTypes validation deprecation warning from SideEffect.render

When rendering components using withSideEffect, I am getting the following warning from React (15.0.2):

Warning: You are manually calling a React.PropTypes validation function for the {prop} prop on {component}. This is deprecated and will not work in the next major version. You may be seeing this warning due to a third-party PropTypes library. See https://fb.me/react-warning-dont-call-proptypes for details.
    at printWarning (.../node_modules/fbjs/lib/warning.js:36:17)
    at warning (.../node_modules/fbjs/lib/warning.js:60:22)
    at checkType (.../node_modules/react/lib/ReactPropTypes.js:134:51)
    at checkPropTypes (.../node_modules/react/lib/ReactElementValidator.js:178:36)
    at validatePropTypes (.../node_modules/react/lib/ReactElementValidator.js:208:5)
    at Object.ReactElementValidator.createElement (.../node_modules/react/lib/ReactElementValidator.js:242:5)
    at SideEffect.render (.../node_modules/react-side-effect/lib/index.js:102:35)
    at [object Object].ReactCompositeComponentMixin._renderValidatedComponentWithoutOwnerOrContext (.../node_modules/react/lib/ReactCompositeComponent.js:815:34)
    at [object Object].ReactCompositeComponentMixin._renderValidatedComponent (.../node_modules/react/lib/ReactCompositeComponent.js:841:32)

It looks like this is possibly a false positive on the detection of manual propTypes validation.

prevent handleStateChangeOnClient being called for server rendered items

I'm creating a DataLayer component to interact with google tag manager in an isomorphic way.

And thus far this works great, because I collect the events using my DataLayer component, and generate a javascript snippet with the dataLayer variable set to the rewinded data of the component.

But when the client application loads, it calls handleStateChangeOnClient with the same values, even though the properties of the rendered components didn't change.

Is there a way to circumvent this?

API suggestion: deprecate rewind() and use a closure to prevent leaks and race conditions

This is a proposal to deprecate the current API for server-side rendering:

let markup = ReactDOM.renderToString(<App />);
let title = DocumentTitle.rewind();

and replace it with this, inspired by Aphrodite's SSR API:

let { title, markup } = DocumentTitle.renderStatic(
  () => ReactDOM.renderToString(<App />)
);

This API has two advantages. It is impossible to forget to call rewind. It is also easy to throw an error if DocumentTitle.renderStatic is called in a reentrant way.

You could use this with Aphrodite easily too. The markup field in the result is whatever you return from the closure (can figure out a better name later -- I just didn't want to call it "html" since it might include CSS):

let { title, markup: { html, css } } = DocumentTitle.renderStatic(
  // Aphrodite.renderStatic() returns { html, css }
  () => Aphrodite.renderStatic(() => ReactDOM.renderToString(<App />))
);

This is easy to build in user-space but I think it might be a strictly better API than rewind() so wanted to start a discussion here.

How about this

Manage stateful data is painful, but with some functional pattern math power, we can do it simply,
See this
For now, It just bind API for stateful data, but I will implement it for Redux.

Memory Leak on Server Render?

When analyzing our heaps on our server, the array mountedInstances gets larger and larger with each check, even with manually triggering garbage collection. This eventually takes up a LOT of memory.

We are currently using react-document-title within our isomorphic components, which in turn uses this library.

When we are on the server, it seems like we shouldn't need to keep track of mounted instances at all, right? Is that within the scope of this library to handle, or should we be accounting for this in our implementation?

Happy to help work on solution, just wanted to see if you think this is an issue with the library or not.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.