RickWong / react-transmit

Relay-inspired library based on Promises instead of GraphQL.
BSD 3-Clause "New" or "Revised" License
1.31k stars 61 forks source link

Using `this.props.params` from React-Router within a fragment function #45

Closed carlosvillu closed 8 years ago

carlosvillu commented 8 years ago

I have a component which, within its ComponentDidMount function, uses this.props.params.id to make an asynchronous call to an API and show results.

I'd like to replicate this behavior in a fragment function but the execution context (this) does not represent the component instance but the function. Therefore, I can't access this.props.

What should I do to use the information provided by React-Router within the fragment functions?

Thx!

RickWong commented 8 years ago

The parent component that renders <TransmitContainer /> should pass the variables prop in.

carlosvillu commented 8 years ago

Thx @RickWong

Sorry but I dont understand. Do you means something like this:

export default class App extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    const children = this.props.children;
    console.log(children);
    return (
      <div className="App" style={{display: 'flex'}}>
        <div className="Pokemons">
          <Pokemons _onFetch={(data) => console.log('onFetch', data)}/>
        </div>
        <div>
          {children ? <children variables={{id: this.props.params.pkdx_id}}/> : "Elige un pokemon"}
        </div>
      </div>
    );
  }
}

With a component like this, to show the details:

class PokemonDetail extends React.Component {

  constructor(...args){
    super(...args);
    this.state = {pokemon: LOADING_POKEMON};
  }

  //componentDidMount(){
  //  pokemonAPI({id: this.props.params.pkdx_id}).then(pokemon => this.setState({pokemon}));
  //}

  componentWillReceiveProps(props){
    this.setState({pokemon: LOADING_POKEMON});
    pokemonAPI({id: props.params.pkdx_id}).then(pokemon => this.setState({pokemon}));
  }

  render(){
    console.log('PokemonDetail Render')
    return(
      <div className="">
        <div>
          {!this.state.pokemon.sprites
              ? <img src="http://placeholdit.imgix.net/~text?txtsize=33&txt=P&w=84&h=84" width="84" height="84"/>
              : this.state.pokemon.sprites.map((sprite, index) => <img key={index} src={`${POKEAPI_HOST}${sprite}`} width="84" height="84"/>)
          }
        </div>
        <div>
          {this.state.pokemon.name}
        </div>
      </div>
    );
  }
}

export default Transmit.createContainer(PokemonDetail, {
  initialVariables: {
        oPokemon: {},
    id: null
    },
    fragments: {
    pokemon: function({oPokemon, id}){
      return pokemonAPI({id}).catch(console.error.bind(console))
    }
  }
});

And a rute files like this:

export default (
  <Router>
    <Route path="/" component={App}>
      <Route path="pokemon/:pkdx_id" component={PokemonDetail}/>
    </Route>
  </Router>    
);

But that didnt work, because this.props.children is not TransmitContainer.

BTW, the Pokemons component work fine:

class Pokemons extends React.Component {

  constructor(...args){
    super(...args);
    this.state = {pokemons: this.props.pokemons || [] }
  }

  render(){
    return(
      <ul>
        { 
          !this.state.pokemons.length ? <p>Cargando lista de pokemons</p>
                                      : this.state.pokemons.map((pokemon, index) => {
                                          return(
                                            <li key={index}>
                                              <Link to={`/pokemon/${pokemon.pkdx_id}`}>{pokemon.name}</Link>
                                            </li>
                                          );
                                        })
        }
      </ul>
    );
  }
}

export default Transmit.createContainer(Pokemons, {
  initialVariables: {
        aPokemons: [],
    },
    fragments: {
    pokemons: function({aPokemons}){
      return fetch('http://pokeapi.co/api/v1/pokemon/?limit=50')
              .then(resp => resp.json())
              .then( pokemonsList => {
                return aPokemons.concat(pokemonsList.objects)
              })
              .catch(console.error.bind(console));
    }
  }
});

Where is my misunderstood ?!

RickWong commented 8 years ago

Create an extra routing component like this:

/**
 * PokemonDetailRoute.js
 */
import PokemonDetail from "./PokemonDetail";

export default class PokemonDetailRoute extends React.Component {
    render () {
        // Pass `variables` prop with `pkdx_id` to the Transmit Container.
        return <PokemonDetail variables={id: this.props.params.pkdx_id} />;
    }
};

/**
 * Routes.js
 */
// Configure the new routing component in React Router routes.
import PokemonDetailRoute from "./PokemonDetailRoute";

export default (
  <Router>
    <Route path="/" component={App}>
      <Route path="pokemon/:pkdx_id" component={PokemonDetailRoute} />
    </Route>
  </Router>    
);
lucasmogari commented 8 years ago

I was looking for a way to do that too. @RickWong suggestion worked!

I've created a function to use in my application.

import React from 'react';
import Transmit from 'react-transmit';
import assign from 'libs/assign';

export default function (Component, options) {
  return React.createClass({
    displayName: (Component.displayName || Component.name) + "RouteWrapper",
    render: function () {
      const props = this.props;
      const variables = {
        history: props.history,
        location: props.location,
        params: props.params,
        route: props.route,
        routeParams: props.routeParams,
        routes: props.routes,
        children: props.children
      };

      return React.createElement(
        Transmit.createContainer(Component, options),
        assign({variables}, props)
      );
    }
  });
}
carlosvillu commented 8 years ago

Hi @lucasmogari

I have to donde something wrong, because for me is not working. Do you have any example online to see it ?!

carlosvillu commented 8 years ago

@RickWong Will be possible use the instance of the component like the fragment context ?!

lucasmogari commented 8 years ago

Hi @carlosvillu, I created an example here: https://github.com/lucasmogari/react-isomorphic-starterkit

carlosvillu commented 8 years ago

Hi @lucasmogari, thx for sharing. But in my case, the problems come when I have more than one route. Now my router looks like this:

  <Router>
    <Route path="/" component={App}>
      <IndexRoute component={Index} />
      <Route path="pokemon/:pkdx_id" component={PokemonDetailRoute}/>
      <Route path="*" component={NotFound} />
    </Route>
  </Router>    

I don't know why the fragments are called several times in the same render. First it renders the page and then resolves the fragments. I don't know exactly why; I suppose that is my fail. But I really don't know what is failing.

Here is what I tried:

https://github.com/carlosvillu-com/intro-isomorphic-app/tree/feature/isomorphic/app

:(

RickWong commented 8 years ago

@carlosvillu There is no need to track this.state.pokemon here https://github.com/carlosvillu-com/intro-isomorphic-app/blob/feature/isomorphic/app/components/pokemonDetail.js#L26 Because Transmit will not render the component if it hasn't resolved this.props.pokemon anyway. So always use this.props.pokemon.

You should pass the renderLoading prop on this line: https://github.com/carlosvillu-com/intro-isomorphic-app/blob/feature/isomorphic/app/routes.js#L17

    return <PokemonDetail variables={{id: this.props.params.pkdx_id}} renderLoading={
() => <PokemonDetail pokemon={[LOADING_POKEMON]} /> // Render loading PokemonDetail by passing the `pokemon` prop with hard-coded data.
} />; 
carlosvillu commented 8 years ago

Thx @RickWong I will do the changes and figure out why I have severals reloads when I go to /pokemon/:id page.

RickWong commented 8 years ago

I'm closing this issue. New questions can go in new issues.

carlosvillu commented 8 years ago

Thx @RickWong for the help :)