GithubHelp home page GithubHelp logo

Comments (5)

markalfred avatar markalfred commented on May 28, 2024

Hey @jononu, when you say

if the shape generator took the prop name or ideally a path

could you give an example of what this might look like? Does "path" refer to a file path, or an object-tree path?

I don't ๐Ÿ’ฏ grok the question, so just trying to get a bit more detail on what you're thinking in terms of inputs and expected outputs.

That said, the intent of this library is to generate values based on PropTypes, so in the context of components themselves. It's not so much interested in generating state, but rather props. Often times the two will share similar or identical shapes, but not necessarily.

Any additional info you can provide will help me determine if it's something this lib can/should do, or maybe suggest alternatives.

Thanks!

from react-generate-props.

jononu avatar jononu commented on May 28, 2024

I'd like to be able to return something for a particular prop, not all props of a particular type. To do that, I was thinking it could match on the object tree path and return a particular result based on that path.

This would be used in our tests, so we don't have to duplicate all the props when testing the component. We could generate the props in the test and simply change the particular values the test is interested in.

someReducer

export initialState = { 
  foo: 'bar',
  asdf: 'jklm'
};

... reducer-y things

In our tests

import { initialState } from './someReducer';

props = generateProps(propTypes, { 
  generators: { 
    shape: (shape, path) => {
      if (path === 'store.someReducer`) {
        return initialState;
      }
      return generateProps(shape, path);
    } 
  }
})
props.store.someReducer.foo = 'baz'

const wrapper = shallow(<TestedComponent {...props} />);

... describe / expect-y things

I think ideally we'd make it smart enough to find the correct initialState automatically based on the object tree path.

from react-generate-props.

markalfred avatar markalfred commented on May 28, 2024

That makes sense, thank you @jononu. The only issue I think I see here is that it's based on the assumption that a PropType.shape will share the same shape as a reducer's โ€”ย which might not always, and in many cases will never, be the case. It essentially assumes that there are no complex "selectors" between the state and the component.

For instance, it'd be reasonable for your state to look like this

state = {
  posts: [
    { id: 1, title: 'A Post' },
    { id: 2, title: 'Another Post' }
  ],
  comments:  [
    { id: 1, postId: 1, text: 'Great post.' },
    { id: 2, postId: 1, text: 'Agreed.' },
    { id: 3, postId: 2, text: 'Another great post.' }
  ]
}

But for your component to have PropTypes like this:

Post.propTypes = {
  id: PropTypes.number.isRequired,
  title: PropTypes.string.isRequired,
  comments: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      text: PropTypes.string.isRequired
    }).isRequired
  )
}

So the shape of your state looks more like a relational DB, but the shape of your Post component's props is more streamlined to simplify rendering. Depending on how you select a post and its comments, your selector might need to be something complex like

mapStateToProps = (state, ownProps) => {
  const currentPost = _.find(state.posts, { id: ownProps.currentPostId })
  const currentComments = _.filter(state.comments, { postId: ownProps.currentPostId })
  return { ...currentPost, comments: currentComments }
}

I think it'd be difficult to try to derive the correct state to generate in the shape generator given a situation like this. This is all just a long-winded way of saying that this feels like it might be outside the scope of this library's duties since it feels tied to a particular state/component relationship that some apps may not have.

However, if you do have a component whose state matches some part of your app's state tree, I think the simplest solution would be to (1) generate your initial state, (2) generate default props for your component, and then (3) merge the two objects. The simplest way would be using something like lodash's merge.

I just added this test scenario to ensure this would produce the expected results:

// The component takes props A through E. 
// Each one is a shape containing a String in "..._a", and a Shape in "..._b".
// The Shape in "..._b" contains a String in its own "..._a".
const propTypes = {
  a: PropTypes.shape({ a_a: PropTypes.string.isRequired, a_b: PropTypes.shape({ a_b_a: PropTypes.string.isRequired }).isRequired }).isRequired,
  b: PropTypes.shape({ b_a: PropTypes.string.isRequired, b_b: PropTypes.shape({ b_b_a: PropTypes.string.isRequired }).isRequired }).isRequired,
  c: PropTypes.shape({ c_a: PropTypes.string.isRequired, c_b: PropTypes.shape({ c_b_a: PropTypes.string.isRequired }).isRequired }).isRequired,
  d: PropTypes.shape({ d_a: PropTypes.string.isRequired, d_b: PropTypes.shape({ d_b_a: PropTypes.string.isRequired }).isRequired }).isRequired,
  e: PropTypes.shape({ e_a: PropTypes.string.isRequired, e_b: PropTypes.shape({ e_b_a: PropTypes.string.isRequired }).isRequired }).isRequired,
}

// Initial state has all potential shapes of this prop type definition,
// from fully-defined to undefined.
const initialState = {
  a: { a_a: 'initialState', a_b: { a_b_a: 'initialState' } },
  b: { b_a: 'initialState', b_b: {} },
  c: { c_a: 'initialState' },
  d: {}
}

// Where a value was defined in initialState, it should be used.
// Where one didn't exist, the generateProps value should be used instead.
const expected = {
  a: { a_a: 'initialState', a_b: { a_b_a: 'initialState' } },
  b: { b_a: 'initialState', b_b: { b_b_a: 'string' } },
  c: { c_a: 'initialState', c_b: { c_b_a: 'string' } },
  d: { d_a: 'string', d_b: { d_b_a: 'string' } },
  e: { e_a: 'string', e_b: { e_b_a: 'string' } },
}

const props = generateProps(propTypes)
const merged = _.merge(props, initialState)

merged.should.deep.equal(expected)

โœ“

Do you think you could adapt this technique to your needs?

Thanks for submitting this issue, and let me know if I've misunderstood or if this doesn't cover your usecase!


Full disclosure, I actually spent some time trying to create a recursive custom shape generator with initialState and generateProps fallbacks as described. It turned out to be unnecessarily complex and maybe impossible given current constraints. I'm leaving my attempts below for posterity.

Click to expand

I believe that the shape generator's override should give you the ability to generate a shape based on the shape of your state, and catered to your own app.

import generateProps from 'react-generate-props'
import { initialState } from './someReducer'
// initialState === { foo: 'bar', asdf: 'jklm' }

import Component from './someComponent'
// Component.propTypes === { foo: PropTypes.string.isRequired }

// For each key in the propTypes definition, pull the value from initialState:
const simpleShapeGenerator = shape => _.mapValues(shape, (v, k) => initialState[k])

const props = generateProps(Component, { generators: { shape: simpleShapeGenerator } })
// props === { foo: 'bar' }

That all works to pull in some initialState via the generator override, but it's not doing anything particularly useful โ€”ย you could have just passed initialState directly to Component and gotten the same general results. The usefulness seems to come in when we can use the standard generators for fallbacks...

// For each key in the propTypes definition, pull the value from initialState,
// but if a value doesn't exist there, generate a default one:
const complexShapeGenerator = shape =>
  _.mapValues(shape, (v, k) => initialState[k] || generateProps({ [k]: v })[k])

Component.propTypes = {
  foo: PropTypes.string.isRequired,
  someNewKey: PropTypes.string.isRequired
}

// initialState === { foo: 'bar', asdf: 'jklm' }

const props = generateProps(Component, { generators: { shape: complexShapeGenerator } })
// props === { foo: 'bar', someNewKey: 'string' }

The generateProps({ [k]: v })[k] syntax is a little weird, and it's only necessary because generateProps refuses to take an argument that isn't an object right now. So you can't just call generateProps(PropTypes.string.isRequired) and get back 'string'. That's something that I'd like to change (and I just created a ticket for it: #18).

But... this works. However, it doesn't recursively call generateProps for any values missing in initialState.

we-need-to-go-deeper.jpg

const recursiveShapeGenerator = shape =>
  _.mapValues(shape, (v, k) => 
    initialState[k] || 
    generateProps({ [k]: v }, { generators: { shape: recursiveShapeGenerator } })[k]
  )

This works in this case:

pts = {
a: PropTypes.shape({
  b: PropTypes.shape({
    c: PropTypes.shape({
      d: PropTypes.shape({
        e: PropTypes.string.isRequired
      }).isRequired
    }).isRequired
  }).isRequired
}).isRequired
}

initialState = { a: undefined }

but breaks here...

initialState = { a: {} }

Ok... rethinking...

from react-generate-props.

jononu avatar jononu commented on May 28, 2024

Apologies for the slow reply.

We're pretty selective (hah!) on our use of selectors. Most of our store state is kept simple and used directly by components.

The _.merge approach or something like it would probably work for me. I want to build something framework-y that doesn't require developers to write their own code to merge initialState with the generated props. I could probably use this library to generate an initial set of props, then inspect those props to find known properties that could be replaced with various reducer initialState objects.

Something like

const propTypes = {
  foo: PropTypes.string.isRequired,
  store: PropTypes.shape({
    reducer1: PropTypes.shape({
      bar: PropTypes.string.isRequired
    }),
    reducer2: PropTypes.shape({
      baz: PropTypes.string.isRequired,
    })
  }).isRequired
}

const props = generateProps(propTypes)

if (props.store) {
  Object.keys(props.store).forEach((propName) => {
    props.store[propName] = reducers[propName].initialState;  // assume reducers is an object of all the reducers
  });
}

I'm good with that approach if you think this feature is out of scope for this project. Knowing what prop you're generating in a custom generator seemed more generally useful.

Thanks for going deep and prototyping a solution!

from react-generate-props.

markalfred avatar markalfred commented on May 28, 2024

@jononu just a note: since you're using Redux and its combine reducers functionality, you can generate an entire state tree made up of all reducers' initial states by simply calling your top-level reducer. I've done this as a way to programmatically test every selector throughout an entire application, and ensure that our initial states are actually valid.

So rather than hooking directly to reducers[name].initialState, you could generate your entire application's state tree like...

import reducers from 'reducers/index.js'
const fullInitialState = reducers({}, {})

At that point, fullInitialState will contain the entire state tree for your application, but where each branch of that tree is made up of its reducer's initial state. Rather than reducers[propName].initialState you should be able to access the same values at fullInitialState[propName], and you won't need to worry about exporting initialState from all your reducers and assembling it into that reducers object.

Let me know if any of this doesn't suit your needs, I'm happy to try to find another solution if we think one deserves to make its way into this lib, or if it feels like there's an extension to this lib that might be useful!

from react-generate-props.

Related Issues (18)

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.