Project Templates
Social Network
Alimentación e Interacciones para tu Red Social
40 min
introducción en este tutorial, aprenderás cómo construir un feed de red social con características interactivas como publicaciones, comentarios y me gusta utilizando back4app como tu servicio de backend estas características forman el núcleo de cualquier aplicación de redes sociales, permitiendo a los usuarios compartir contenido e interactuar con otros usuarios back4app es una plataforma de backend as a service (baas) construida sobre parse server que proporciona a los desarrolladores una infraestructura robusta para crear aplicaciones escalables sin gestionar el código del lado del servidor sus capacidades de base de datos en tiempo real y la gestión de usuarios integrada lo convierten en una excelente opción para desarrollar aplicaciones de redes sociales al final de este tutorial, habrás implementado un feed social completamente funcional que permite a los usuarios crear publicaciones con texto e imágenes ver publicaciones de otros usuarios en un feed dar me gusta a las publicaciones comentar en las publicaciones ver detalles de la publicación con comentarios aprenderás cómo estructurar eficientemente tus clases de base de datos, implementar actualizaciones en tiempo real y crear una interfaz de usuario receptiva que proporcione una experiencia fluida en todos los dispositivos requisitos previos para completar este tutorial, necesitarás una cuenta de back4app puedes registrarte para obtener una cuenta gratuita en back4app com https //www back4app com un proyecto de back4app configurado puedes aprender a crear un nuevo proyecto siguiendo nuestra guía de introducción a back4app https //www back4app com/docs/get started/welcome node js instalado en tu máquina local conocimientos básicos de javascript y react js un sistema de autenticación funcional si aún no has configurado uno, sigue nuestro sistema de autenticación para redes sociales tutorial primero familiaridad con conceptos modernos de desarrollo web (componentes, gestión de estado, etc ) proyecto back4gram encuentra aquí el código completo para un proyecto de muestra de red social construido con back4app paso 1 – entendiendo el modelo de datos antes de escribir cualquier código, es importante entender el modelo de datos para nuestra red social necesitaremos varias clases de parse para construir la funcionalidad de nuestro feed clase de publicación la publicación clase almacenará toda la información relacionada con las publicaciones autor un puntero al usuario que creó la publicación contenido el contenido de texto de la publicación imagen un archivo de imagen opcional me gusta un número que representa cuántos me gusta tiene la publicación comentarios un arreglo de punteros a objetos de comentario (opcional, se puede consultar por separado) creadoen gestionado automáticamente por back4app actualizadoen gestionado automáticamente por back4app clase comentario el comentario clase almacenará comentarios en publicaciones autor un puntero al usuario que escribió el comentario publicación un puntero a la publicación en la que se comenta contenido el contenido de texto del comentario creadoen gestionado automáticamente por back4app actualizadoen gestionado automáticamente por back4app seguimiento de me gusta tenemos dos opciones para rastrear los me gusta contador simple almacenar un conteo de me gusta en cada publicación (más fácil pero menos detallado) registros de me gusta crear una clase separada me gusta que rastree cada me gusta individual (más detallado) para este tutorial, implementaremos ambos enfoques, comenzando con el contador más simple y luego mostrando cómo implementar registros de me gusta individuales para características más avanzadas comencemos configurando estas clases en back4app paso 2 – configuración del modelo de datos en back4app en este paso, crearás las clases necesarias en tu base de datos de back4app creando la clase post inicia sesión en tu panel de back4app y navega a tu proyecto en la barra lateral izquierda, haz clic en "base de datos" para abrir el navegador de la base de datos haz clic en el botón "crear una clase" en la parte superior de la página en el modal que aparece, ingresa "post" como el nombre de la clase y selecciona "personalizado" como el tipo luego haz clic en "crear clase" \[imagen modal de crear clase de back4app con "post" ingresado como nombre de clase] ahora, agrega las siguientes columnas a tu clase post haz clic en "agregar columna" crea las siguientes columnas autor (tipo puntero, clase objetivo user) contenido (tipo cadena) imagen (tipo archivo) me gusta (tipo número, valor por defecto 0) creando la clase comentario haz clic en "crear una clase" nuevamente ingresa "comentario" como el nombre de la clase y selecciona "personalizado" como el tipo agrega las siguientes columnas autor (tipo puntero, clase objetivo user) publicación (tipo puntero, clase objetivo post) contenido (tipo cadena) configuración de permisos a nivel de clase por seguridad, configuremos los permisos apropiados para nuestras clases en el navegador de base de datos, selecciona la clase post haz clic en el botón "seguridad" configura los permisos lectura pública sí (cualquiera puede ver las publicaciones) escritura pública no (solo los usuarios autenticados pueden crear publicaciones) agregar campo público no buscar, obtener, crear, actualizar, eliminar requiere autenticación de usuario repite configuraciones similares para la clase comment ahora que tenemos nuestro modelo de datos configurado, implementemos los componentes de front end para nuestro feed de red social la página de noticias es el corazón de cualquier red social muestra publicaciones de los usuarios y permite la interacción creamos un componente de feed que obtiene y muestra publicaciones crea un archivo llamado src/pages/feedpage js import react, { usestate, useeffect, useref } from 'react'; import { usenavigate } from 'react router dom'; import { box, flex, vstack, heading, text, button, input, simplegrid, avatar, hstack, iconbutton, textarea, spinner, center, image, } from '@chakra ui/react'; import { link as routerlink } from 'react router dom'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function feedpage() { const \[searchquery, setsearchquery] = usestate(''); const \[newpostcontent, setnewpostcontent] = usestate(''); const \[posts, setposts] = usestate(\[]); const \[isloading, setisloading] = usestate(true); const \[isposting, setisposting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // new state variables for image upload const \[selectedimage, setselectedimage] = usestate(null); const \[imagepreview, setimagepreview] = usestate(null); const fileinputref = useref(null); const navigate = usenavigate(); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { // redirect to login if not authenticated navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // handle image selection const handleimagechange = (e) => { if (e target files && e target files\[0]) { const file = e target files\[0]; setselectedimage(file); // create a preview url const reader = new filereader(); reader onloadend = () => { setimagepreview(reader result); }; reader readasdataurl(file); } }; // clear selected image const handleclearimage = () => { setselectedimage(null); setimagepreview(null); if (fileinputref current) { fileinputref current value = ''; } }; // fetch posts useeffect(() => { const fetchposts = async () => { if (!currentuser) { console log('no current user, skipping post fetch'); return; } setisloading(true); console log('fetching posts for user ', currentuser id); try { // create a query for the post class const query = new parse query('post'); console log('created post query'); // include the user who created the post query include('author'); console log('including author in query'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query console log('executing query '); const results = await query find(); console log('query results received ', results length, 'posts found'); // convert parse objects to plain objects const fetchedposts = \[]; for (let i = 0; i < results length; i++) { const post = results\[i]; try { const author = post get('author'); console log(`processing post ${i+1}/${results length}, author `, author ? author id 'null'); if (!author) { console warn(`post ${post id} has no author, skipping`); continue; } const postobj = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; fetchedposts push(postobj); } catch (posterror) { console error(`error processing post ${i+1} `, posterror); console error('post data ', post tojson()); } } console log('successfully processed', fetchedposts length, 'posts'); setposts(fetchedposts); } catch (error) { console error('error fetching posts ', error); console error('error details ', error code, error message); toaster create({ title 'error loading posts', description error message, type 'error', }); } finally { setisloading(false); console log('post loading completed'); } }; fetchposts(); }, \[currentuser, navigate]); // function to create a new post with image const handlecreatepost = async () => { if (!newpostcontent trim() && !selectedimage) return; setisposting(true); console log('creating new post '); try { // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { console log('uploading image '); const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); console log('image uploaded successfully'); } console log('post object created, saving to database '); // save the post const savedpost = await post save(); console log('post saved successfully with id ', savedpost id); // add the new post to the state const newpost = { id savedpost id, content savedpost get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, image savedpost get('image') ? savedpost get('image') url() null, likes 0, createdat savedpost get('createdat') }; console log('adding new post to state'); setposts(\[newpost, posts]); setnewpostcontent(''); setselectedimage(null); setimagepreview(null); toaster create({ title 'post created', description 'your post has been published successfully!', type 'success', }); } catch (error) { console error('error creating post ', error); console error('error details ', error code, error message); toaster create({ title 'error creating post', description error message, type 'error', }); } finally { setisposting(false); console log('post creation completed'); } }; // function to like a post const handlelikepost = async (postid) => { try { // get the post const query = new parse query('post'); const post = await query get(postid); // increment likes post increment('likes'); await post save(); // update the post in the state setposts(posts map(p => { if (p id === postid) { return { p, likes p likes + 1 }; } return p; })); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // function to logout const handlelogout = async () => { try { await parse user logout(); navigate('/login'); } catch (error) { console error('error logging out ', error); } }; const handlesearchsubmit = (e) => { e preventdefault(); if (searchquery trim()) { navigate(`/search?q=${encodeuricomponent(searchquery)}`); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; return ( \<flex direction="row" h="100vh"> {/ left sidebar (navigation) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderright="1px solid" bordercolor="gray 600" \> \<vstack align="stretch" spacing={4}> \<heading size="md">social network\</heading> \<button as={routerlink} to="/feed" variant="ghost" justifycontent="flex start"> home \</button> \<button as={routerlink} to="/search" variant="ghost" justifycontent="flex start"> search \</button> \<button as={routerlink} to="/messages" variant="ghost" justifycontent="flex start"> messages \</button> \<button as={routerlink} to="/profile" variant="ghost" justifycontent="flex start"> profile \</button> \<button onclick={handlelogout} variant="ghost" colorscheme="red" justifycontent="flex start"> logout \</button> \</vstack> \</box> {/ main content (feed) /} \<box flex="1" p={4} overflowy="auto"> \<form onsubmit={handlesearchsubmit}> \<input placeholder="search " value={searchquery} onchange={(e) => setsearchquery(e target value)} mb={4} /> \</form> {/ create post with image upload /} \<box border="1px solid" bordercolor="gray 600" p={4} borderradius="md" mb={6}> \<textarea placeholder="what's on your mind?" value={newpostcontent} onchange={(e) => setnewpostcontent(e target value)} mb={2} resize="none" /> {/ image preview /} {imagepreview && ( \<box position="relative" mb={2}> \<image src={imagepreview} alt="preview" maxh="200px" borderradius="md" /> \<button position="absolute" top="2" right="2" size="sm" colorscheme="red" onclick={handleclearimage} \> remove \</button> \</box> )} \<box> \<input type="file" accept="image/ " onchange={handleimagechange} style={{ display 'none' }} ref={fileinputref} id="image upload" /> \<button as="label" htmlfor="image upload" cursor="pointer" variant="outline" size="sm" mr={2} mb={0} \> 📷 add photo \</button> \</box> \<button colorscheme="blue" onclick={handlecreatepost} isloading={isposting} disabled={(!newpostcontent trim() && !selectedimage) || isposting} \> post \</button> \</box> {/ posts feed with images /} {isloading ? ( \<center py={10}> \<spinner size="xl" /> \</center> ) posts length > 0 ? ( \<vstack align="stretch" spacing={4}> {posts map(post => ( \<box key={post id} border="1px solid" bordercolor="gray 600" p={4} borderradius="md" \> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(post createdat)}\</text> \</hstack> \<text mb={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box mb={4}> \<image src={post image} alt="post image" borderradius="md" maxh="400px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" size="sm" onclick={() => handlelikepost(post id)} \> ❤️ {post likes} \</button> \<button variant="ghost" size="sm" as={routerlink} to={`/post/${post id}`} \> 💬 comment \</button> \<button variant="ghost" size="sm"> 🔄 share \</button> \</hstack> \</box> ))} \</vstack> ) ( \<text>no posts yet follow users or create your first post!\</text> )} \</box> {/ right sidebar (trending hashtags) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderleft="1px solid" bordercolor="gray 600" \> \<heading size="md" mb={4}> trending today \</heading> \<simplegrid columns={1} spacing={2}> \<button variant="outline" size="sm" colorscheme="whitealpha">#travel\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#tech\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#foodie\</button> \</simplegrid> \</box> \</flex> ); } export default feedpage; este componente de feed incluye varias características clave verificación de autenticación asegura que solo los usuarios registrados puedan ver el feed creación de publicaciones permite a los usuarios crear nuevas publicaciones con texto e imágenes opcionales carga de imágenes maneja la selección, vista previa y carga de imágenes usando parse file visualización del feed muestra publicaciones de todos los usuarios en orden cronológico inverso funcionalidad de me gusta permite a los usuarios dar me gusta a las publicaciones usando un enfoque de contador simple navegación proporciona enlaces a otras partes de la aplicación examinemos cómo back4app nos está ayudando a implementar estas características creación de publicaciones con parse back4app hace que crear publicaciones con imágenes sea simple // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); } // save the post const savedpost = await post save(); parse file maneja automáticamente las cargas de archivos, almacenamiento y generación de url, facilitando la adición de imágenes a las publicaciones recuperación de publicaciones con consulta de parse el sistema de consultas de back4app facilita la obtención y visualización de publicaciones // create a query for the post class const query = new parse query('post'); // include the user who created the post query include('author'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query const results = await query find(); el include('author') método es particularmente poderoso, ya que incluye automáticamente el objeto user referenciado con cada publicación, reduciendo la necesidad de múltiples consultas proyecto back4gram encuentra aquí el código completo para un proyecto de muestra de red social construido con back4app paso 4 – implementación de la vista de detalle de publicación con comentarios ahora, vamos a crear una página de detalle de publicación que muestre una sola publicación con sus comentarios y permita a los usuarios agregar nuevos comentarios crea un archivo llamado src/pages/postdetailspage js import react, { usestate, useeffect } from 'react'; import { useparams, usenavigate, link as routerlink } from 'react router dom'; import { box, flex, vstack, heading, text, button, avatar, hstack, textarea, spinner, center, image } from '@chakra ui/react'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function postdetailspage() { const { id } = useparams(); const navigate = usenavigate(); const \[post, setpost] = usestate(null); const \[comments, setcomments] = usestate(\[]); const \[newcomment, setnewcomment] = usestate(''); const \[isloading, setisloading] = usestate(true); const \[iscommenting, setiscommenting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // fetch post and comments useeffect(() => { const fetchpostdetails = async () => { if (!currentuser) return; setisloading(true); try { // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); // convert post to plain object const fetchedpost = { id postobject id, content postobject get('content'), author { id postobject get('author') id, username postobject get('author') get('username'), avatar postobject get('author') get('avatar') ? postobject get('author') get('avatar') url() null }, likes postobject get('likes') || 0, createdat postobject get('createdat'), image postobject get('image') ? postobject get('image') url() null }; // convert comments to plain objects const fetchedcomments = commentresults map(comment => ({ id comment id, content comment get('content'), author { id comment get('author') id, username comment get('author') get('username'), avatar comment get('author') get('avatar') ? comment get('author') get('avatar') url() null }, createdat comment get('createdat') })); setpost(fetchedpost); setcomments(fetchedcomments); } catch (error) { console error('error fetching post details ', error); toaster create({ title 'error loading post', description error message, type 'error', }); navigate('/feed'); } finally { setisloading(false); } }; if (id) { fetchpostdetails(); } }, \[id, currentuser, navigate]); // function to add a comment const handleaddcomment = async () => { if (!newcomment trim()) return; setiscommenting(true); try { // get the post object const postquery = new parse query('post'); const postobject = await postquery get(id); // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); // add the new comment to the state const newcommentobj = { id comment id, content comment get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, createdat comment get('createdat') }; setcomments(\[ comments, newcommentobj]); setnewcomment(''); toaster create({ title 'comment added', description 'your comment has been posted successfully!', type 'success', }); } catch (error) { console error('error adding comment ', error); toaster create({ title 'error adding comment', description error message, type 'error', }); } finally { setiscommenting(false); } }; // function to like the post const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // increment likes postobject increment('likes'); await postobject save(); // update the post in the state setpost({ post, likes post likes + 1 }); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; if (isloading) { return ( \<center h="100vh"> \<spinner size="xl" /> \</center> ); } if (!post) { return ( \<center h="100vh"> \<vstack> \<text>post not found\</text> \<button as={routerlink} to="/feed" colorscheme="blue"> back to feed \</button> \</vstack> \</center> ); } return ( \<box maxw="800px" mx="auto" p={4}> \<button as={routerlink} to="/feed" mb={4} variant="ghost"> ← back to feed \</button> {/ post /} \<box border="1px solid" bordercolor="gray 600" p={6} borderradius="md" mb={6}> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<vstack align="start" spacing={0}> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">{formatdate(post createdat)}\</text> \</vstack> \</hstack> \<text fontsize="lg" my={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box my={4}> \<image src={post image} alt="post image" borderradius="md" maxh="500px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" onclick={handlelikepost} \> ❤️ {post likes} \</button> \<button variant="ghost"> 🔄 share \</button> \</hstack> \</box> {/ comments section /} \<box> \<heading size="md" mb={4}> comments ({comments length}) \</heading> {/ add comment /} \<box mb={6}> \<textarea placeholder="write a comment " value={newcomment} onchange={(e) => setnewcomment(e target value)} mb={2} /> \<button colorscheme="blue" onclick={handleaddcomment} isloading={iscommenting} disabled={!newcomment trim()} \> post comment \</button> \</box> \<hr style={{ marginbottom "1rem" }} /> {/ comments list /} {comments length > 0 ? ( \<vstack align="stretch" spacing={4}> {comments map(comment => ( \<box key={comment id} p={4} bg="gray 700" borderradius="md"> \<hstack mb={2}> \<avatar src={comment author avatar} name={comment author username} size="sm" /> \<text fontweight="bold">{comment author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(comment createdat)}\</text> \</hstack> \<text>{comment content}\</text> \</box> ))} \</vstack> ) ( \<text color="gray 400">no comments yet be the first to comment!\</text> )} \</box> \</box> ); } export default postdetailspage; this post detail page provides several important features 1\ post display shows the complete post with author information, content, image, and likes 2\ comment creation allows users to add new comments to the post 3\ comment listing displays all comments in chronological order 4\ like functionality enables users to like the post let's examine the key aspects of how back4app is helping us implement these features \### fetching post details with related objects back4app makes it easy to fetch a post with related information ```javascript // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); el include('author') método recupera automáticamente el objeto user relacionado junto con la publicación, eliminando la necesidad de consultas separadas consultando comentarios relacionados el sistema de consultas de back4app nos permite encontrar todos los comentarios relacionados con una publicación específica // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); el equalto('post', postobject) método filtra los comentarios para que solo se muestren los relacionados con la publicación actual creando comentarios con punteros crear un comentario con una relación a una publicación es sencillo // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); este es un ejemplo de cómo crear un comentario relacionado con una publicación el post campo se establece como un puntero al objeto de la publicación, estableciendo la relación entre el comentario y la publicación proyecto back4gram encuentra aquí el código completo para un proyecto de muestra de red social construido con back4app paso 5 – implementando funcionalidad avanzada de me gusta con parse el contador de me gusta simple que hemos implementado hasta ahora funciona bien para la funcionalidad básica, pero tiene limitaciones los usuarios pueden dar me gusta a una publicación múltiples veces un usuario no puede "quitar el me gusta" a una publicación no podemos mostrar quién le dio me gusta a una publicación creamos un sistema de me gusta más avanzado utilizando una clase de me gusta para rastrear los me gusta individuales configurando la clase de me gusta primero, crea una nueva clase en tu panel de control de back4app haz clic en "crear una clase" en el navegador de base de datos ingresa "me gusta" como el nombre de la clase y selecciona "personalizado" como el tipo agrega las siguientes columnas usuario (tipo puntero, clase objetivo usuario) publicación (tipo puntero, clase objetivo publicación) implementando la gestión de me gusta en nuestro código ahora, actualicemos nuestra funcionalidad de me gusta en el postdetailspage js archivo // add this state at the beginning of the component const \[hasliked, sethasliked] = usestate(false); // add this to the useeffect that fetches post details // check if current user has liked this post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const userlike = await likequery first(); sethasliked(!!userlike); // replace the handlelikepost function with this new version const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // check if user has already liked the post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const existinglike = await likequery first(); if (existinglike) { // user already liked the post, so remove the like await existinglike destroy(); // decrement likes count on the post postobject increment('likes', 1); await postobject save(); // update ui sethasliked(false); setpost({ post, likes post likes 1 }); } else { // user hasn't liked the post yet, so add a like const like = parse object extend('like'); const like = new like(); like set('user', currentuser); like set('post', postobject); await like save(); // increment likes count on the post postobject increment('likes'); await postobject save(); // update ui sethasliked(true); setpost({ post, likes post likes + 1 }); } } catch (error) { console error('error toggling like ', error); toaster create({ title 'error', description 'could not process your like please try again ', type 'error', }); } }; luego, actualiza el botón de me gusta para reflejar el estado actual \<button variant="ghost" onclick={handlelikepost} color={hasliked ? "red 500" "inherit"} \> {hasliked ? "❤️" "🤍"} {post likes} \</button> esta implementación rastrea los me gusta individuales utilizando un me gusta clase permite a los usuarios alternar me gusta (dar me gusta y quitar me gusta) indica visualmente si el usuario actual ha dado me gusta a la publicación mantiene el conteo de me gusta en la publicación para consultas eficientes mostrando quién le gustó una publicación también puedes agregar funcionalidad para mostrar quién ha dado me gusta a una publicación // add this state const \[likeusers, setlikeusers] = usestate(\[]); const \[showlikes, setshowlikes] = usestate(false); // add this function const fetchlikes = async () => { try { const postquery = new parse query('post'); const postobject = await postquery get(id); const likequery = new parse query('like'); likequery equalto('post', postobject); likequery include('user'); const likes = await likequery find(); const users = likes map(like => { const user = like get('user'); return { id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null }; }); setlikeusers(users); setshowlikes(true); } catch (error) { console error('error fetching likes ', error); } }; luego agrega un elemento de interfaz de usuario para mostrar los usuarios que le dieron me gusta a la publicación \<text color="blue 400" cursor="pointer" onclick={fetchlikes} fontsize="sm" \> see who liked this post \</text> {showlikes && ( \<box mt={2} p={2} bg="gray 700" borderradius="md"> \<heading size="xs" mb={2}>liked by \</heading> {likeusers length > 0 ? ( \<vstack align="stretch"> {likeusers map(user => ( \<hstack key={user id}> \<avatar size="xs" src={user avatar} name={user username} /> \<text fontsize="sm">{user username}\</text> \</hstack> ))} \</vstack> ) ( \<text fontsize="sm">no likes yet\</text> )} \</box> )} proyecto back4gram encuentra aquí el código completo para un proyecto de muestra de red social construido con back4app paso 6 – implementando actualizaciones en tiempo real con parse live query una de las características más poderosas de back4app es la capacidad de recibir actualizaciones en tiempo real con live query esto permite que tu aplicación se actualice automáticamente cuando se crean nuevas publicaciones o comentarios sin necesidad de refrescar manualmente configurando live query en back4app primero, necesitas habilitar live query para tu aplicación en back4app ve a tu panel de back4app navega a configuración de la app > configuración del servidor desplázate hacia abajo hasta "nombres de clases de live query de parse" agrega "post" y "comment" a la lista de clases guarda tus cambios implementando live query para publicaciones en tiempo real actualicemos nuestro feedpage js para incluir live query para actualizaciones de publicaciones en tiempo real // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser) return; try { console log('setting up live query for posts'); // create a query that will be used for the subscription const query = new parse query('post'); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened'); }); // handle new posts livequerysubscription current on('create', (post) => { console log('new post received via live query ', post id); // skip posts from the current user as they're already in the ui if (post get('author') id === currentuser id) { console log('skipping post from current user'); return; } // format the new post const author = post get('author'); const newpost = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; // add the new post to the posts state setposts(prevposts => \[newpost, prevposts]); }); // handle post updates livequerysubscription current on('update', (post) => { console log('post updated via live query ', post id); // format the updated post const newlikes = post get('likes') || 0; // update the post in the state setposts(prevposts => prevposts map(p => p id === post id ? { p, likes newlikes } p ) ); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser]); implementando live query para comentarios en tiempo real de manera similar, actualicemos nuestro postdetailspage js para incluir live query para comentarios en tiempo real // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser || !id) return; try { console log('setting up live query for comments'); // get the post object to use in the query const postquery = new parse query('post'); const postobject = await postquery get(id); // create a query that will be used for the subscription const query = new parse query('comment'); query equalto('post', postobject); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query for comments'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened'); }); // handle new comments livequerysubscription current on('create', (comment) => { console log('new comment received via live query ', comment id); // skip comments from the current user as they're already in the ui if (comment get('author') id === currentuser id) { console log('skipping comment from current user'); return; } // format the new comment const author = comment get('author'); const newcomment = { id comment id, content comment get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, createdat comment get('createdat') }; // add the new comment to the comments state setcomments(prevcomments => \[ prevcomments, newcomment]); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query for comments ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser, id]); con estas implementaciones de live query, tu red social ahora se actualizará en tiempo real las nuevas publicaciones de otros usuarios aparecerán automáticamente en el feed los recuentos de me gusta se actualizarán en tiempo real cuando otros usuarios le den me gusta a una publicación los nuevos comentarios aparecerán automáticamente en la página de detalles de la publicación proyecto back4gram encuentra aquí el código completo para un proyecto de muestra de red social construido con back4app paso 7 – consultas avanzadas y optimización del rendimiento a medida que tu red social crece, el rendimiento se vuelve cada vez más importante exploremos algunas técnicas avanzadas para optimizar tu aplicación utilizando las potentes capacidades de consulta de back4app implementación de paginación en lugar de cargar todas las publicaciones a la vez, implementa la paginación para cargarlas en lotes // add these state variables const \[page, setpage] = usestate(0); const \[hasmoreposts, sethasmoreposts] = usestate(true); const postsperpage = 10; // update your fetchposts function const fetchposts = async (loadmore = false) => { if (!currentuser) return; if (!loadmore) { setisloading(true); } try { const query = new parse query('post'); query include('author'); query descending('createdat'); query limit(postsperpage); query skip(page postsperpage); const results = await query find(); // process and format posts as before const fetchedposts = \[ ]; // update the hasmoreposts state sethasmoreposts(results length === postsperpage); // update the posts state if (loadmore) { setposts(prevposts => \[ prevposts, fetchedposts]); } else { setposts(fetchedposts); } // update the page if this was a "load more" request if (loadmore) { setpage(prevpage => prevpage + 1); } } catch (error) { // handle errors } finally { setisloading(false); } }; // add a load more button at the bottom of your posts list {hasmoreposts && ( \<button onclick={() => fetchposts(true)} variant="outline" isloading={isloading} mt={4} \> load more posts \</button> )} implementando un feed específico para el usuario para una experiencia más personalizada, es posible que desees mostrar publicaciones solo de los usuarios que el usuario actual sigue // assuming you have a "follow" class with "follower" and "following" pointers const fetchfollowingposts = async () => { try { // first, find all users that the current user follows const followquery = new parse query('follow'); followquery equalto('follower', currentuser); const followings = await followquery find(); // extract the users being followed const followedusers = followings map(follow => follow\ get('following')); // add the current user to show their posts too followedusers push(currentuser); // create a query for posts from these users const query = new parse query('post'); query containedin('author', followedusers); query include('author'); query descending('createdat'); query limit(20); const results = await query find(); // process results } catch (error) { // handle errors } }; uso de cloud functions para operaciones complejas para operaciones complejas que requerirían múltiples consultas, considera usar las cloud functions de back4app ve a tu panel de back4app navega a cloud code > cloud functions crea una nueva función // example function to get a feed with posts, likes, and comment counts parse cloud define("getfeed", async (request) => { try { const user = request user; const page = request params page || 0; const limit = request params limit || 10; if (!user) { throw new error("user must be logged in"); } // query for posts const query = new parse query("post"); query include("author"); query descending("createdat"); query limit(limit); query skip(page limit); const posts = await query find({ usemasterkey true }); // format posts and get additional info const formattedposts = await promise all(posts map(async (post) => { // check if user has liked this post const likequery = new parse query("like"); likequery equalto("post", post); likequery equalto("user", user); const hasliked = await likequery count({ usemasterkey true }) > 0; // get comment count const commentquery = new parse query("comment"); commentquery equalto("post", post); const commentcount = await commentquery count({ usemasterkey true }); // format post for response return { id post id, content post get("content"), image post get("image") ? post get("image") url() null, author { id post get("author") id, username post get("author") get("username"), avatar post get("author") get("avatar") ? post get("author") get("avatar") url() null }, likes post get("likes") || 0, hasliked hasliked, commentcount commentcount, createdat post get("createdat") }; })); return { posts formattedposts }; } catch (error) { throw new error(`error fetching feed ${error message}`); } }); luego llama a esta función en la nube desde tu aplicación const fetchfeed = async () => { try { setisloading(true); const result = await parse cloud run('getfeed', { page 0, limit 10 }); setposts(result posts); } catch (error) { console error('error fetching feed ', error); toaster create({ title 'error loading feed', description error message, type 'error', }); } finally { setisloading(false); } }; este enfoque puede reducir significativamente el número de consultas a la base de datos y mejorar el rendimiento proyecto back4gram encuentra aquí el código completo para un proyecto de muestra de red social construido con back4app conclusión en este tutorial, has construido un feed de red social integral con características interactivas utilizando back4app recapitulemos lo que has logrado diseño del modelo de datos creado un modelo de datos estructurado para publicaciones, comentarios y me gusta implementación del feed construido una página de feed que muestra publicaciones de todos los usuarios creación de publicaciones implementada la funcionalidad para que los usuarios creen publicaciones con texto e imágenes sistema de comentarios creado un sistema de comentarios que permite a los usuarios interactuar con las publicaciones funcionalidad de me gusta implementados sistemas de me gusta tanto simples como avanzados actualizaciones en tiempo real agregada live query para actualizaciones en tiempo real de publicaciones y comentarios optimización del rendimiento exploradas técnicas para mejorar el rendimiento a medida que tu aplicación escala back4app ha proporcionado una base de datos segura y escalable para almacenar contenido generado por los usuarios almacenamiento de archivos para imágenes de publicaciones autenticación de usuario integrada para mayor seguridad consulta en vivo para actualizaciones en tiempo real funciones en la nube para operaciones complejas de backend con estos bloques de construcción en su lugar, su red social tiene una base sólida que puede escalar a medida que crece su base de usuarios próximos pasos para mejorar aún más su red social, considere implementar estas características notificaciones alertar a los usuarios cuando sus publicaciones reciban me gusta o comentarios sistema de seguimiento de usuarios permitir a los usuarios seguirse entre sí y personalizar sus feeds hashtags y búsqueda implementar un sistema de hashtags y funcionalidad de búsqueda mejorada publicaciones ricas en medios soportar videos, múltiples imágenes y otros tipos de medios analíticas rastrear la participación y el comportamiento del usuario para mejorar su aplicación para el código completo de la aplicación de red social back4gram, puede consultar el repositorio de github https //github com/templates back4app/back4gram al aprovechar los poderosos servicios de backend de back4app, puede centrarse en crear una gran experiencia de usuario mientras la plataforma maneja la infraestructura compleja necesaria para una aplicación de red social