En este tutorial aprenderemos a consumir una restfull api en un proyecto con React y Redux. Crearemos una aplicación para mostrar un listado de posts, ademas de poder agregar, modificar y eliminar posts.
¿Qué necesitamos para comenzar?
- Conocer y saber utilizar ES6
- Conocimientos básicos de React
- Conocimiento básico de redux
- Node.js >=v6 en tu computadora
Partiremos de un repositorio que tiene ya una página para mostrar posts y nosotros solo agregaremos la funcionalidad de consultar los posts desde la api de jsonPlaceholder. Para ello, vamos a clonar el repositorio.
$ git clone https://github.com/Clau8a/redux-ejemplo.git nombre_proyecto
Una vez clonado el proyecto podemos correrlo para ver su funcionalidad.
$ npm run start
Ahora que tenemos nuestro repositorio analicemos un poco el ejemplo, estamos utilizando la vista App.js para desplegar los posts e interactuar con ellos, además tenemos un archivo post.js que tiene las mecánicas de comportamiento para un post. Los posts están estructurados en formato json y tienen cuatro propiedades un userId, id, title, y body, por lo tanto esas propiedades utilizaremos para modificarlos.
Lo que vamos a hacer es agregar un archivo para crear las funciones HTTP. Asi que, dentro de la carpeta src
creamos una carpeta llamada lib
y dentro creamos un archivo con el nombre de api.js.
Para hacer llamadas utilizaremos la función fetch.
La función fetch es una alternativa a XMLHttpRequest
, fetch regresa una promesa que no será rechazada como error HTTP, en cambio ésta se resolverá, siempre y cuando no haya un fallo en la red.
Enviaremos dos parámetros en la función fetch, url y request. La variable url la formaremos con una base que es la url del servidor y le concatenaremos los parámetros necesarios para cada tipo de petición y en el request pondremos el método HTTP, el body y los headers.
Como mencionamos anteriormente, utilizaremos jsonPlaceholder , que es una api gratuita para regresar datos en formato json, por lo tanto vamos a agregar una variable con la url del servidor:
//variable url del servidor const BASEURL = "https://jsonplaceholder.typicode.com";
A continuación tenemos un ejemplo de una función para agregar un post, en ella recibimos un objeto como parámetro llamado post (este contiene las propiedades userId, id, title, y body), formamos la url concatenando BASEURL (url del servidor) más la ruta a consultar, después creamos el objeto request al cual le agregamos el método HTTP POST, como body le mandamos el parámetro post y utilizamos JSON.stringify() y especificamos que el contenido que mandaremos es tipo json, por último hacemos la petición con el fetch el cual nos regresará un objeto que transformaremos a json.
export const apiAddPost = (post) => { const url = BASEURL + "/posts/"; const request = { method:'POST', body: JSON.stringify(post), headers: {"Content-type":"application/json; charset=UTF-8"} }; return fetch(url, request) .then(response => response.json()); };
Agregaremos lo siguiente a nuestro archivo api.js (archivo api.js completo)
[codepen_embed height=”265″ theme_id=”dark” slug_hash=”djMJNm” default_tab=”js” user=”8aclau”]See the Pen <a href=’https://codepen.io/8aclau/pen/djMJNm/’>api.js</a> by Claudia (<a href=’https://codepen.io/8aclau’>@8aclau</a>) on <a href=’https://codepen.io’>CodePen</a>.[/codepen_embed]
Ya tenemos nuestras peticiones listas ahora necesitamos comenzar con nuestros reducers. Por ello, agregaremos Redux.js a nuestro proyecto.
$ npm i redux
Agregaremos también react-redux para hacer nuestro enlazamiento de datos con las vistas.
$ npm i react-redux
Y agregamos también un middleware para facilitar el manejo del dispatch de Redux
$ npm i redux-thunk
En la carpeta src agregamos una carpeta llamada reducers y dentro de ella agregamos un archivo postReducer.js. En este archivo vamos a importar las funciones del archivo api.js
import {apiGetPost, apiAddPost, apiDeletePost, apiUpdatePost} from '../lib/api';
Después vamos a agregar nuestro estado inicial en el cual solo tendremos una lista de posts
const initialState = {posts: []};
Agregamos los tipos de actions que serán cuatro y las actions
//tipos de actions const GET_POSTS = 'GET_POSTS'; const ADD_POST = 'ADD_POST'; const UPDATE_POST = 'UPDATE_POST'; const DELETE_POST = 'DELETE_POST'; //actions const getPosts = (posts) => ({ type: GET_POSTS, payload: posts }); const addPost = (post) => ({ type: ADD_POST, payload: post }); const updatePost = (post) => ({ type: UPDATE_POST, payload: post }); const deletePost = (id) => ({ type: DELETE_POST, payload: id });
Agregamos las funciones que utilizaremos en nuestras vistas para hacer las peticiones y que serán las responsables de desencadenar nuestras actions.
export const fetchGetPost = () => { //redux-thunk nos permite mandar el dispatch como parámetro return (dispatch) => { //Llamamos a la función de la api apiGetPost() .then(res => //al reolverse la petición de manera correcta desencadenamos la acción // getPosts enviando los posts recibidos {dispatch(getPosts(res));}) .catch(res => {console.log(res);}) } };
Agregamos la función para hacer las modificaciones en el store, la cual debe ser una función pura, que pueda exportarse y que considere todos los tipos de acciones.
¿Qué es una función pura?
Uno de los principios de Redux es realizar cambios mediante el uso de funciones puras. Una función pura es una función que regresa un valor que es procesado basado en sus argumentos. Las funciones puras toman al menos un argumento y siempre regresan un valor. Otra de las características de las funciones puras es que no alteran variables globales o cambian algún elemento del estado de la aplicación.
export default (state = initialState, action) => { switch (action.type) { case GET_POSTS: return { ...state, posts:action.payload }; case ADD_POST: return {...state, posts: [action.payload, ...state.posts]}; case UPDATE_POST: return {...state, posts: [...state.posts.map((post) => post.id === action.payload.id ? action.payload:post)]} case DELETE_POST: return {...state, post: [...state.posts.filter(elem=>elem.id!==action.payload)]} default: return { ...state };} }
//importamos las funciones de redux import { createStore, applyMiddleware, combineReducers } from 'redux'; import thunk from 'redux-thunk'; //importamos nuestro reducer import postsReducer from './postsReducer'; //creamos un reducer principal que combina todos los reducer que realicemos //en este caso solo tenemos un reducer const mainReducer = combineReducers({posts:postsReducer}); //creamos el store especificando que utilizamos thunk export default createStore(mainReducer,applyMiddleware(thunk));
Es momento de agregar el store a nuestra aplicación para ello es necesario modifica nuestro index.js. Primero agregaremos provider y el store
import { Provider } from 'react-redux'; import store from './reducers/store';
Modificamos el render para asignar el store
ReactDOM.render( <Provider store={store}> <App/> </Provider>, document.getElementById('root') );
Ya es hora de hacer modificaciones a la vista para hacer uso de nuestro reducer. Para conectar nuestra vista con el reducer primero debemos agregar una librería que nos permita hacerlo.
$ npm i react-redux
Ahora en el archivo App.js vamos a importar connect y también las funciones del postReducer.js.
import { connect } from 'react-redux'; import { fetchGetPost, fetchAddPost, fetchUpdatePost, fetchDeletePost } from './reducers/postsReducer';
Eliminamos el arreglo posts y modificamos el export default de app.js para hacer uso de connect, vamos a agregar dos parámetros a connect, el primero será una función que mapeará las propiedades del state de redux a las cuales queremos tener acceso y las insertaremos como propiedades para el componente y en el segundo parámetro haremos un mapeo de las funciones que harán modificaciones al state.
export default connect( //función que mapea propiedades del state con propiedades del componente (state) => ({posts:state.posts.posts,}), //mapeo de funciones {fetchGetPost, fetchAddPost, fetchUpdatePost, fetchDeletePost}) (App);
El patrón que se sigue en la función donde mapeamos las propiedades del state es el siguiente
(state) => ({nombre_propiedad: state.nombre_reducer.propiedad})
Para hacer uso de la propiedad posts vamos agregar una variable en el render de App justo antes de comenzar el return
const posts = this.props.posts;
El último paso para poder correr nuestra aplicación es hacer la consulta de posts, para ello agregaremos el método componentDidMount y desde ahí haremos la consulta.
componentDidMount(){this.props.fetchGetPost();}
Ahora correremos nuestra aplicación con
$ npm run start
Y ya estamos consumiendo una api !
Bien ahora solo nos faltan tres interacciones, continuemos con editar algún post existente para ello vamos a mandar una propiedad en el componente <Post />, llamaremos a la propiedad update y le asignaremos como valor la función del reducer para hacer un update, quedando de la siguiente manera
<Post {...post} update={this.props.fetchUpdatePost} />
Ahora modificaremos el archivo post.js, en el metodo handleSave cambiaremos su funcionalidad, haremos desde ahí la actualización, haciendo uso de la propiedad update. El método handleSave nos queda de la siguiente manera
handleSave() { this.props.update(this.props.id, { title: this.state.title, body: this.state.body }) this.setState({ editing: false }) }
Y con esto ahora podemos editar posts en la api.
Ahora vamos a eliminar post, haremos algo similar, agregaremos una propiedad delete en el elemento <Post /> y le asignamos fetchDeletePost, y nos queda de esta manera
<Post {...post} update={this.props.fetchUpdatePost} delete={this.props.fetchDeletePost} />
Regresamos a post.js y eliminaremos la propiedad delete del state para no agregar la clase hide a los post eliminados, en su lugar los eliminaremos desde la api, y nos quedaria de esta manera
<div className={"col-xs-12 post " + (this.state.deleted ? " hide" : "")}> <div className="col-xs-12 post">
Modificamos ahora handleDelete
handleDelete() {this.props.delete(this.props.id);}
Y es así como ahora se están eliminando nuestros posts al hacer click en el botón de eliminar, recuerda que esta es una aplicación de ejemplo jsonPlaceHolder no elimina o modifica en su servidor los posts realmente por lo tanto si haces un refresh a la página los posts que eliminaste o modificaste volverán a su estado original.
Ya casi terminamos, ahora solo nos falta poder agregar posts nuevos y no tenemos una interfaz donde agregarlos así que vamos a agregar el siguiente formulario en App.js antes del listado de posts
<div className="App-intro"> <div className="col-xs-12"> <h2>Agregar Post</h2> <form onSubmit={this.handleAdd.bind(this)}> <input name="title" placeholder="Título" className="form-control" value={this.state.title} onChange={this.hanldeChange.bind(this)}/> <textarea name="body" placeholder="Contenido" className="form-control" onChange={this.hanldeChange.bind(this)} value={this.state.body} /> <div className="col-xs-12 button tar"> <button className="btn btn-success" type="submit">Agregar</button> </div> </form> </div> <h2>Posts</h2>
Necesitamos agregar al componente App un state para agregar el title y el body, además de un handleChange para manejar estas propiedades.
constructor(props) { super(props); this.state= {title:'',body:''} } hanldeChange(event) { let name=event.target.name; let value=event.target.value; this.setState({ [`${name}`]:value }); }
Además en el form agregamos una propiedad onSubmit que ejecuta la función handleAdd, así que hay que definirla.
handleAdd(event){ event.preventDefault(); this.props.fetchAddPost({ title: this.state.title, body: this.state.body, userId: 1}); this.setState({ title:'', body:'' }); };
De esta manera ya podemos agregar posts en nuestra aplicación y estos se agregaran al inicio de la lista.
¡Eureka! Hemos terminado.
Si quieres ver el código completo puedes verlo en la rama solution
$ git checkout solution
Si quieres aprender más sobre redux, fetch o connect te dejo los siguientes links: