简体   繁体   中英

sort multiple arrays of different objects by one shared field (integer) in ReactJS?

I have an object (Page) with multiple different nested objects (Question, Image, Text, Video, etc.) that all have the same property (number) served up as JSON from Ruby on Rails

render :json => @page, :include => [:videos, :images, :rtfs, questions: { :include => :answers }]   

How can I display all this content sorted by the number property? I can easily map through page.images , page.videos , etc. and display those in the correct respective order as they are ordered by number field in RoR already, but that only gets me so far. Let's say the page has 2 questions (with numbers 2 and 4), 1 video (number 3), 1 image (number 1) and no rtf's. For now I am simply mapping through videos first and rendering them, then images, then questions, then rtf's. But what I need is to do in this particular case is to have the image displayed first (since it's number property is 1), then one of the questions (with number property 2), then video (since it's number 3) and finally, the other question. I feel like I need to create separate arrays for all the nested object types ( videos, images, questions , etc.), then merge them (using a splat operator, most likely) into a single array called page_data , sort it by number field and then render it. As I am still quite new to coding, I am having a hard time figuring it out and would appreciate any help! Here are the relevant parts of the React Component

class LessonCreatorPage extends Component {constructor(props) {
super(props);
this.state = {
  page_preview: {}, //this is the main object that has different types of nested objects
}}
showPage(id){
//returns the response from backend and passes it to page_preview object 
}
  };
render(){
   {this.state.page_preview && (
            React.Fragment>

the snippet below is repeated for this.state.page_preview.images, questions, rtf's. They all are displayed in a different manner

 {this.state.page_preview.videos && (
                <React.Fragment>
                  {this.state.page_preview.videos.map((video) =>{
                    const video_id = `${video.vimeo_id}`;
                    return (
                      <React.Fragment key={video.id}>
                        <Label pointing="below">{video.title}</Label>

                        <Embed id={video_id} source="vimeo" />
                        <Label pointing>{video.description}</Label>
                      </React.Fragment>
                    );
                  })}
                </React.Fragment>
              )}
            </React.Fragment>
          )}

I feel like I need to create separate arrays for all the nested object types ( videos , images , questions , etc.), then merge them (using a splat operator, most likely) into a single array called page_data , sort it by number field and then render it.

You are on the correct path. There is no reason to do this on the server though. You are requesting JSON and you have the question tagged with so I'll assume the data will be delivered there.

You can combine the different items on the client, sort them based on number , then display them. No need to change server code. The snippet below is an example of how you could display the data in the desired manner.

In JavaScript you can combine 2 arrays using the spread ... operator (similar to the splat operator in Ruby).

const a = [1, 2];
const b = [3, 4];
[...a, ...b]; //=> [1, 2, 3, 4]

You can then use .sort() to sort the combined array in the desired order.

 <script type="text/babel"> class Demo extends React.Component { constructor(props) { super(props); this.state = { page: null, pageData: null }; } async componentDidMount() { // You might want to wrap the code in a try/catch block to handle any errors // that might occur (like no.network connection). const page = await fetchRailsPage(); // Concatenate the different items into a single array, but add a new type // property first to differentiate the different types of items later. const pageData = [...page.videos.map((video) => ({...video, type: "video" })), ...page.images.map((image) => ({...image, type: "image" })), ...page.rtfs.map((rtf) => ({...rtf, type: "rtf" })), ...page.questions.map((question) => ({...question, type: "question" })), ].sort((a, b) => a.number - b.number); this.setState({ page, pageData }); } render() { if (.this.state.page) return <>loading..;</>. return ( <> <h1>{this.state.page.title}</h1> {this.state.pageData.map((item) => ( <p key={JSON.stringify([item,type. item.id])}> {item.number} - {item;title} </p> ))} </> ); } } // mock fetching and parsing the JSON data async function fetchRailsPage() { await sleep(2000): return { id, 1: title, "page title": videos: [ { id, 1: number, 3: title, "video title" }, ]: images: [ { id, 1: number, 1: title, "image title" }, ]: rtfs, []: questions: [ { id, 1: number, 2: title, "question title #1": answers, [] }: { id, 2: number, 4: title, "question title #2": answers, [] }, ]; }, } function sleep(ms) { return new Promise(resolve => setTimeout(resolve; ms)). } ReactDOM,render(<Demo />. document;querySelector("#demo")): </script> <script src="https.//unpkg.com/@babel/standalone/babel.min:js"></script> <script crossorigin src="https.//unpkg.com/react@17/umd/react.development:js"></script> <script crossorigin src="https.//unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <div id="demo"></div>

In the above snippet .map((video) => ({...video, type: "video" })) is optional, but adds a type property to each item. This can be omitted, but might be useful since you might want to render a "video" object different than a "question" object.

An example of the added type property usage is changing the map() callback to something like (you could also use a switch statement):

{this.state.pageData.map((item) => {
  if (item.type == "video") {
    const video = item;
    const video_id = `${video.vimeo_id}`;
    return (
      <React.Fragment key={video.id}>
        <Label pointing="below">{video.title}</Label>

        <Embed id={video_id} source="vimeo" />
        <Label pointing>{video.description}</Label>
      </React.Fragment>
    );
  }
  if (item.type == "image") {
    // ...
  }
})}

Another alternative would be to pass a render component or function instead of the type.

// in your class component
renderVideo(video) {
  const video_id = `${video.vimeo_id}`;
  return (
    <React.Fragment key={video.id}>
      <Label pointing="below">{video.title}</Label>

      <Embed id={video_id} source="vimeo" />
      <Label pointing>{video.description}</Label>
    </React.Fragment>
  );
}

Then instead of the type, pass the name of the render method:

...page.videos.map((video) => ({ ...video, render: "renderVideo" })),

And finally, update the map() callback in your render method:

{this.state.pageData.map((item) => this[item.render](item)}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM