Sort objects in array with dynamic nested property keys












6















I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.



So far I've got this code



sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';

if(isReverse) return valueB.localeCompare(valueA);

return valueA.localeCompare(valueB);
})
}));
}


At this point the keys are hardcoded ['general']['orderID'] but I want this part to be dynamic by adding a keys param to the sortBy function:



sortBy = (keys, isReverse=false) => { ...


keys is an array with the nested keys. For the above example, it will be ['general', 'fileID'].



What are the steps that need to be taken to make this dynamic?



Note: child objects can be undefined therefore I'm using a || {}



Note 2: I'm using es6. No external packages.










share|improve this question























  • will it contain just two keys or that also can be dynamic

    – Shubham Khatri
    4 hours ago











  • Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic

    – Thore
    4 hours ago


















6















I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.



So far I've got this code



sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';

if(isReverse) return valueB.localeCompare(valueA);

return valueA.localeCompare(valueB);
})
}));
}


At this point the keys are hardcoded ['general']['orderID'] but I want this part to be dynamic by adding a keys param to the sortBy function:



sortBy = (keys, isReverse=false) => { ...


keys is an array with the nested keys. For the above example, it will be ['general', 'fileID'].



What are the steps that need to be taken to make this dynamic?



Note: child objects can be undefined therefore I'm using a || {}



Note 2: I'm using es6. No external packages.










share|improve this question























  • will it contain just two keys or that also can be dynamic

    – Shubham Khatri
    4 hours ago











  • Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic

    – Thore
    4 hours ago
















6












6








6








I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.



So far I've got this code



sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';

if(isReverse) return valueB.localeCompare(valueA);

return valueA.localeCompare(valueB);
})
}));
}


At this point the keys are hardcoded ['general']['orderID'] but I want this part to be dynamic by adding a keys param to the sortBy function:



sortBy = (keys, isReverse=false) => { ...


keys is an array with the nested keys. For the above example, it will be ['general', 'fileID'].



What are the steps that need to be taken to make this dynamic?



Note: child objects can be undefined therefore I'm using a || {}



Note 2: I'm using es6. No external packages.










share|improve this question














I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.



So far I've got this code



sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';

if(isReverse) return valueB.localeCompare(valueA);

return valueA.localeCompare(valueB);
})
}));
}


At this point the keys are hardcoded ['general']['orderID'] but I want this part to be dynamic by adding a keys param to the sortBy function:



sortBy = (keys, isReverse=false) => { ...


keys is an array with the nested keys. For the above example, it will be ['general', 'fileID'].



What are the steps that need to be taken to make this dynamic?



Note: child objects can be undefined therefore I'm using a || {}



Note 2: I'm using es6. No external packages.







javascript arrays sorting object ecmascript-6






share|improve this question













share|improve this question











share|improve this question




share|improve this question










asked 4 hours ago









ThoreThore

393113




393113













  • will it contain just two keys or that also can be dynamic

    – Shubham Khatri
    4 hours ago











  • Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic

    – Thore
    4 hours ago





















  • will it contain just two keys or that also can be dynamic

    – Shubham Khatri
    4 hours ago











  • Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic

    – Thore
    4 hours ago



















will it contain just two keys or that also can be dynamic

– Shubham Khatri
4 hours ago





will it contain just two keys or that also can be dynamic

– Shubham Khatri
4 hours ago













Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic

– Thore
4 hours ago







Sorry for not mentioning. In my current project it can be up to 4 keys so it has to be dynamic

– Thore
4 hours ago














7 Answers
7






active

oldest

votes


















3














You can loop ovver the keys to get the values and then compare them like





sortBy = (keys, isReverse=false) => {

this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);

return valueA.localeCompare(valueB);
})
}));
}





share|improve this answer


























  • @ziggywiggy Thanks for pointing out the error

    – Shubham Khatri
    3 hours ago



















2














You can use a loop to extract a nested property path from an object:






const obj = {
a: {
b: {
c: 3
}
}
}

const keys = ['a', 'b', 'c']

let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}

console.log(`c=${value}`);





Then you can wrap the function above into a helper:



function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}


And use it when obtaining your values:



sortBy = (isReverse = false, keys = ) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';

// ...
})
}));
}





share|improve this answer































    1














    One way could be using reduce() over the new keys argument, like this:



    sortBy = (keys, isReverse=false) => {
    this.setState(prevState => ({
    files: prevState.files.sort((a, b) => {
    const valueA = keys.reduce((acc, key) => (acc || {})[key], a) || '';
    const valueA = keys.reduce((acc, key) => (acc || {})[key], b) || '';

    if (isReverse) return valueB.localeCompare(valueA);

    return valueA.localeCompare(valueB);
    })
    }));
    }





    share|improve this answer































      1














      To work with an arbitrary number of keys, you could create a function that could be reused with .reduce() to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.






      const getKey = (o, k) => (o || {})[k];

      const sorter = (isReverse, ...keys) => (a, b) => {
      const valueA = keys.reduce(getKey, a) || '';
      const valueB = keys.reduce(getKey, b) || '';

      if (isReverse) return valueB.localeCompare(valueA);

      return valueA.localeCompare(valueB);
      };

      const sortBy = (isReverse = false, ...keys) => {
      this.setState(prevState => ({
      files: prevState.files.sort(sorter(isReverse, ...keys))
      }));
      }





      I also moved the sort function out to its own const variable, and made it return a new function that uses the isReverse value.






      share|improve this answer

































        0














        Try






        let files = [
        { general: { fileID: "3"}},
        { general: { fileID: "1"}},
        { general: { fileID: "2"}},
        { general: { }}
        ];

        function nestedVal(keys,o) {
        let obj = o;
        keys.forEach(k=> obj = obj[k]||{} );
        return obj.localeCompare ? obj : '';
        }

        function sortBy(keys, arr, isReverse=false) {
        arr.sort((a,b) => {
        let av = nestedVal(keys,a);
        let bv = nestedVal(keys,b);
        let v = av.localeCompare(bv);
        return isReverse ? -v : v;
        })
        }

        sortBy(['general', 'fileID'], files, true)

        console.log(files);








        share|improve this answer

































          0














          This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare might fail.



          sortBy = (keys, isReverse=false) => {
          this.setState(prevState => ({
          files: prevState.files.sort((a, b) => {
          const valueA = getValueAtPath(a, keys);
          const valueB = getValueAtPath(b, keys);

          if(isReverse) return valueB.localeCompare(valueA);

          return valueA.localeCompare(valueB);
          })
          }));
          }

          function getValueAtPath(file, path) {
          let value = file;
          let keys = [...path]; // preserve the original path array

          while(value && keys.length) {
          let key = keys.shift();
          value = value[key];
          }

          return (value || '').toString();
          }





          share|improve this answer

































            0














            In this post, we'll write sortBy as -



            sortBy = (comparator = asc) =>
            this.setState
            ( { files:
            isort
            ( contramap
            ( comparator
            , generalFileId
            )
            , this.state.files
            )
            }
            )


            Your question introduces you to two cool functional concepts; we'll use these to answer the question -




            1. Monads

            2. Contravariant Functors


            Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.



            We'll start by writing a function safeProp that accepts an object and a property string as input. Intuitively, safeProp safely returns the property p of object o -



            const { Nothing, fromNullable } =
            require ('data.maybe')

            const safeProp = (o = {}, p = '') =>

            // if o is an object
            Object (o) === o

            // access property p on object o, wrapping the result in a Maybe
            ? fromNullable (o[p])

            // otherwise o is not an object, return Nothing
            : Nothing ()


            Instead of simply returning o[p] which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -



            const generalFileId = (o = {}) =>

            // access the general property
            safeProp (o, 'general')

            // if it exists, access the fileId property on the child
            .chain (child => safeProp (child, 'fileId'))

            // get the result if valid, otherwise return empty string
            .getOrElse ('')


            Now we have a function which can take objects a varying complexity, and guarantees the result we're interested in -



            console .log
            ( generalFileId ({ general: { fileId: 'a' } }) // 'a'
            , generalFileId ({ general: { fileId: 'b' } }) // 'b'
            , generalFileId ({ general: 'x' }) // ''
            , generalFileId ({ a: 'x '}) // ''
            , generalFileId ({ general: { err: 'x' } }) // ''
            , generalFileId ({}) // ''
            )


            That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.



            I'm intentionally avoiding showing you an implementation of Maybe here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level. This applies regardless of whether the module is provided by a third party, such as from npm, or if you wrote the module yourself.



            We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...





            We start with two basic comparators, asc for ascending sort, and desc for descending sort -



            const asc = (a, b) =>
            a .localeCompare (b)

            const desc = (a, b) =>
            asc (a, b) * -1


            In React, we cannot mutate previous state, instead we must create new state. So to sort, we must implement isort which will not mutate the input object -



            const isort = (compare = asc, xs = ) =>
            xs
            .slice (0) // clone
            .sort (compare) // then sort


            And of course a and b are sometimes complex objects, so case we can't directly call asc or desc. Below contramap will transform our data before using one function g, before passing the data to the other function, f -



            const contramap = (f, g) =>
            (a, b) => f (g (a), g (b))

            const files =
            [ { general: { fileId: 'e' } }
            , { general: { fileId: 'b' } }
            , { general: { fileId: 'd' } }
            , { general: { fileId: 'c' } }
            , { general: { fileId: 'a' } }
            ]

            isort
            ( contramap (asc, generalFileId) // ascending comparator
            , files
            )

            // [ { general: { fileId: 'a' } }
            // , { general: { fileId: 'b' } }
            // , { general: { fileId: 'c' } }
            // , { general: { fileId: 'd' } }
            // , { general: { fileId: 'e' } }
            // ]


            Using the other comparator desc, we can see sorting work in the other direction -



            isort
            ( contramap (desc, generalFileId) // descending comparator
            , files
            )

            // [ { general: { fileId: 'e' } }
            // , { general: { fileId: 'd' } }
            // , { general: { fileId: 'c' } }
            // , { general: { fileId: 'b' } }
            // , { general: { fileId: 'a' } }
            // ]




            Now to write the method for your React component -



            sortBy = (reverse = true) =>
            this.setState
            ( { files:
            isort
            ( contramap
            ( reverse ? desc : asc
            , generalFileId
            )
            , this.state.files
            )
            }
            )


            This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -



            sortBy = (comparator = asc) =>
            this.setState
            ( { files:
            isort
            ( contramap
            ( comparator
            , generalFileId
            )
            , this.state.files
            )
            }
            )




            If the nested property you need to access is not guaranteed to be general and fileId, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -



            const deepProp = (o = {}, props = ) =>
            props .reduce
            ( (acc, p) => // for each p, safely lookup p on child
            acc .chain (child => safeProp (child, p))
            , fromNullable (o) // init with Maybe o
            )

            const generalFileId = (o = {}) =>
            deepProp (o, [ 'general', 'fileId' ]) // using deepProp
            .getOrElse ('')

            const fooBarQux = (o = {}) =>
            deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
            .getOrElse (0) // customizable default

            console.log
            ( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
            , generalFileId ({}) // ''
            , fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
            , fooBarQux ({ foo: { bar: 2 } }) // 0
            , fooBarQux ({}) // 0
            )




            Above, we use the data.maybe package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable, Just and Nothing in a couple dozen lines, which we'll see later in this answer -



            Run the complete demo below on repl.it



            const { Just, Nothing, fromNullable } =
            require ('data.maybe')

            const safeProp = (o = {}, p = '') =>
            Object (o) === o
            ? fromNullable (o[p])
            : Nothing ()

            const generalFileId = (o = {}) =>
            safeProp (o, 'general')
            .chain (child => safeProp (child, 'fileId'))
            .getOrElse ('')

            // ----------------------------------------------
            const asc = (a, b) =>
            a .localeCompare (b)

            const desc = (a, b) =>
            asc (a, b) * -1

            const contramap = (f, g) =>
            (a, b) => f (g (a), g (b))

            const isort = (compare = asc, xs = ) =>
            xs
            .slice (0)
            .sort (compare)

            // ----------------------------------------------
            const files =
            [ { general: { fileId: 'e' } }
            , { general: { fileId: 'b' } }
            , { general: { fileId: 'd' } }
            , { general: { fileId: 'c' } }
            , { general: { fileId: 'a' } }
            ]

            isort
            ( contramap (asc, generalFileId)
            , files
            )

            // [ { general: { fileId: 'a' } }
            // , { general: { fileId: 'b' } }
            // , { general: { fileId: 'c' } }
            // , { general: { fileId: 'd' } }
            // , { general: { fileId: 'e' } }
            // ]


            The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.



            Lastly, sortBy is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse boolean; any valid comparator can be used. This means we could even write an even more specialized comparator that handles tie-breaks using custom logic or compares prop1 first, then prop2, etc. Higher-order functions expand your possibilities tremendously.





            I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way provide access to the module's functionality; all other components of the module are private and free-to-change as the other requirements dictate -



            // Maybe.js
            const None =
            Symbol ()

            class Maybe
            { constructor (v)
            { this.value = v }

            chain (f)
            { return this.value == None ? this : f (this.value) }

            getOrElse (v)
            { return this.value === None ? v : this.value }
            }

            const Nothing = () =>
            new Maybe (None)

            const Just = v =>
            new Maybe (v)

            const fromNullable = v =>
            v == null
            ? Nothing ()
            : Just (v)

            module.exports =
            { Just, Nothing, fromNullable } // note the class is hidden from the user


            Then we would use it in our module. We only have to change the import (require) but everything else just works as-is because the public API of our module matches -



            const { Just, Nothing, fromNullable } =
            require ('./Maybe') // this time, use our own Maybe

            const safeProp = (o = {}, p = '') => // nothing changes here
            Object (o) === o
            ? fromNullable (o[p])
            : Nothing ()

            const deepProp = (o, props) => // nothing changes here
            props .reduce
            ( (acc, p) =>
            acc .chain (child => safeProp (child, p))
            , fromNullable (o)
            )

            // ...




            For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -




            1. multi-sort using contramap

            2. recursive search using contramap






            share|improve this answer

























              Your Answer






              StackExchange.ifUsing("editor", function () {
              StackExchange.using("externalEditor", function () {
              StackExchange.using("snippets", function () {
              StackExchange.snippets.init();
              });
              });
              }, "code-snippets");

              StackExchange.ready(function() {
              var channelOptions = {
              tags: "".split(" "),
              id: "1"
              };
              initTagRenderer("".split(" "), "".split(" "), channelOptions);

              StackExchange.using("externalEditor", function() {
              // Have to fire editor after snippets, if snippets enabled
              if (StackExchange.settings.snippets.snippetsEnabled) {
              StackExchange.using("snippets", function() {
              createEditor();
              });
              }
              else {
              createEditor();
              }
              });

              function createEditor() {
              StackExchange.prepareEditor({
              heartbeatType: 'answer',
              autoActivateHeartbeat: false,
              convertImagesToLinks: true,
              noModals: true,
              showLowRepImageUploadWarning: true,
              reputationToPostImages: 10,
              bindNavPrevention: true,
              postfix: "",
              imageUploader: {
              brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
              contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
              allowUrls: true
              },
              onDemand: true,
              discardSelector: ".discard-answer"
              ,immediatelyShowMarkdownHelp:true
              });


              }
              });














              draft saved

              draft discarded


















              StackExchange.ready(
              function () {
              StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54735985%2fsort-objects-in-array-with-dynamic-nested-property-keys%23new-answer', 'question_page');
              }
              );

              Post as a guest















              Required, but never shown

























              7 Answers
              7






              active

              oldest

              votes








              7 Answers
              7






              active

              oldest

              votes









              active

              oldest

              votes






              active

              oldest

              votes









              3














              You can loop ovver the keys to get the values and then compare them like





              sortBy = (keys, isReverse=false) => {

              this.setState(prevState => ({
              files: prevState.files.sort((a, b) => {
              const clonedKey = [...keys];
              let valueA = a;
              let valueB = b
              while(clonedKey.length > 0) {
              const key = clonedKey.shift();
              valueA = (valueA || {})[key];
              valueB = (valueB || {})[key];
              }
              valueA = valueA || '';
              valueB = valueB || '';
              if(isReverse) return valueB.localeCompare(valueA);

              return valueA.localeCompare(valueB);
              })
              }));
              }





              share|improve this answer


























              • @ziggywiggy Thanks for pointing out the error

                – Shubham Khatri
                3 hours ago
















              3














              You can loop ovver the keys to get the values and then compare them like





              sortBy = (keys, isReverse=false) => {

              this.setState(prevState => ({
              files: prevState.files.sort((a, b) => {
              const clonedKey = [...keys];
              let valueA = a;
              let valueB = b
              while(clonedKey.length > 0) {
              const key = clonedKey.shift();
              valueA = (valueA || {})[key];
              valueB = (valueB || {})[key];
              }
              valueA = valueA || '';
              valueB = valueB || '';
              if(isReverse) return valueB.localeCompare(valueA);

              return valueA.localeCompare(valueB);
              })
              }));
              }





              share|improve this answer


























              • @ziggywiggy Thanks for pointing out the error

                – Shubham Khatri
                3 hours ago














              3












              3








              3







              You can loop ovver the keys to get the values and then compare them like





              sortBy = (keys, isReverse=false) => {

              this.setState(prevState => ({
              files: prevState.files.sort((a, b) => {
              const clonedKey = [...keys];
              let valueA = a;
              let valueB = b
              while(clonedKey.length > 0) {
              const key = clonedKey.shift();
              valueA = (valueA || {})[key];
              valueB = (valueB || {})[key];
              }
              valueA = valueA || '';
              valueB = valueB || '';
              if(isReverse) return valueB.localeCompare(valueA);

              return valueA.localeCompare(valueB);
              })
              }));
              }





              share|improve this answer















              You can loop ovver the keys to get the values and then compare them like





              sortBy = (keys, isReverse=false) => {

              this.setState(prevState => ({
              files: prevState.files.sort((a, b) => {
              const clonedKey = [...keys];
              let valueA = a;
              let valueB = b
              while(clonedKey.length > 0) {
              const key = clonedKey.shift();
              valueA = (valueA || {})[key];
              valueB = (valueB || {})[key];
              }
              valueA = valueA || '';
              valueB = valueB || '';
              if(isReverse) return valueB.localeCompare(valueA);

              return valueA.localeCompare(valueB);
              })
              }));
              }






              share|improve this answer














              share|improve this answer



              share|improve this answer








              edited 3 hours ago

























              answered 4 hours ago









              Shubham KhatriShubham Khatri

              85.3k15103143




              85.3k15103143













              • @ziggywiggy Thanks for pointing out the error

                – Shubham Khatri
                3 hours ago



















              • @ziggywiggy Thanks for pointing out the error

                – Shubham Khatri
                3 hours ago

















              @ziggywiggy Thanks for pointing out the error

              – Shubham Khatri
              3 hours ago





              @ziggywiggy Thanks for pointing out the error

              – Shubham Khatri
              3 hours ago













              2














              You can use a loop to extract a nested property path from an object:






              const obj = {
              a: {
              b: {
              c: 3
              }
              }
              }

              const keys = ['a', 'b', 'c']

              let value = obj;
              for (const key of keys) {
              if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
              value = value[key];
              }

              console.log(`c=${value}`);





              Then you can wrap the function above into a helper:



              function getPath(obj, keys) {
              let value = obj;
              for (const key of keys) {
              if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
              value = value[key];
              }
              return value;
              }


              And use it when obtaining your values:



              sortBy = (isReverse = false, keys = ) => {
              this.setState(prevState => ({
              files: prevState.files.sort((a, b) => {
              const valueA = getPath(a, keys) || '';
              const valueB = getPath(b, keys) || '';

              // ...
              })
              }));
              }





              share|improve this answer




























                2














                You can use a loop to extract a nested property path from an object:






                const obj = {
                a: {
                b: {
                c: 3
                }
                }
                }

                const keys = ['a', 'b', 'c']

                let value = obj;
                for (const key of keys) {
                if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                value = value[key];
                }

                console.log(`c=${value}`);





                Then you can wrap the function above into a helper:



                function getPath(obj, keys) {
                let value = obj;
                for (const key of keys) {
                if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                value = value[key];
                }
                return value;
                }


                And use it when obtaining your values:



                sortBy = (isReverse = false, keys = ) => {
                this.setState(prevState => ({
                files: prevState.files.sort((a, b) => {
                const valueA = getPath(a, keys) || '';
                const valueB = getPath(b, keys) || '';

                // ...
                })
                }));
                }





                share|improve this answer


























                  2












                  2








                  2







                  You can use a loop to extract a nested property path from an object:






                  const obj = {
                  a: {
                  b: {
                  c: 3
                  }
                  }
                  }

                  const keys = ['a', 'b', 'c']

                  let value = obj;
                  for (const key of keys) {
                  if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                  value = value[key];
                  }

                  console.log(`c=${value}`);





                  Then you can wrap the function above into a helper:



                  function getPath(obj, keys) {
                  let value = obj;
                  for (const key of keys) {
                  if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                  value = value[key];
                  }
                  return value;
                  }


                  And use it when obtaining your values:



                  sortBy = (isReverse = false, keys = ) => {
                  this.setState(prevState => ({
                  files: prevState.files.sort((a, b) => {
                  const valueA = getPath(a, keys) || '';
                  const valueB = getPath(b, keys) || '';

                  // ...
                  })
                  }));
                  }





                  share|improve this answer













                  You can use a loop to extract a nested property path from an object:






                  const obj = {
                  a: {
                  b: {
                  c: 3
                  }
                  }
                  }

                  const keys = ['a', 'b', 'c']

                  let value = obj;
                  for (const key of keys) {
                  if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                  value = value[key];
                  }

                  console.log(`c=${value}`);





                  Then you can wrap the function above into a helper:



                  function getPath(obj, keys) {
                  let value = obj;
                  for (const key of keys) {
                  if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                  value = value[key];
                  }
                  return value;
                  }


                  And use it when obtaining your values:



                  sortBy = (isReverse = false, keys = ) => {
                  this.setState(prevState => ({
                  files: prevState.files.sort((a, b) => {
                  const valueA = getPath(a, keys) || '';
                  const valueB = getPath(b, keys) || '';

                  // ...
                  })
                  }));
                  }





                  const obj = {
                  a: {
                  b: {
                  c: 3
                  }
                  }
                  }

                  const keys = ['a', 'b', 'c']

                  let value = obj;
                  for (const key of keys) {
                  if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                  value = value[key];
                  }

                  console.log(`c=${value}`);





                  const obj = {
                  a: {
                  b: {
                  c: 3
                  }
                  }
                  }

                  const keys = ['a', 'b', 'c']

                  let value = obj;
                  for (const key of keys) {
                  if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
                  value = value[key];
                  }

                  console.log(`c=${value}`);






                  share|improve this answer












                  share|improve this answer



                  share|improve this answer










                  answered 4 hours ago









                  nem035nem035

                  25.2k54062




                  25.2k54062























                      1














                      One way could be using reduce() over the new keys argument, like this:



                      sortBy = (keys, isReverse=false) => {
                      this.setState(prevState => ({
                      files: prevState.files.sort((a, b) => {
                      const valueA = keys.reduce((acc, key) => (acc || {})[key], a) || '';
                      const valueA = keys.reduce((acc, key) => (acc || {})[key], b) || '';

                      if (isReverse) return valueB.localeCompare(valueA);

                      return valueA.localeCompare(valueB);
                      })
                      }));
                      }





                      share|improve this answer




























                        1














                        One way could be using reduce() over the new keys argument, like this:



                        sortBy = (keys, isReverse=false) => {
                        this.setState(prevState => ({
                        files: prevState.files.sort((a, b) => {
                        const valueA = keys.reduce((acc, key) => (acc || {})[key], a) || '';
                        const valueA = keys.reduce((acc, key) => (acc || {})[key], b) || '';

                        if (isReverse) return valueB.localeCompare(valueA);

                        return valueA.localeCompare(valueB);
                        })
                        }));
                        }





                        share|improve this answer


























                          1












                          1








                          1







                          One way could be using reduce() over the new keys argument, like this:



                          sortBy = (keys, isReverse=false) => {
                          this.setState(prevState => ({
                          files: prevState.files.sort((a, b) => {
                          const valueA = keys.reduce((acc, key) => (acc || {})[key], a) || '';
                          const valueA = keys.reduce((acc, key) => (acc || {})[key], b) || '';

                          if (isReverse) return valueB.localeCompare(valueA);

                          return valueA.localeCompare(valueB);
                          })
                          }));
                          }





                          share|improve this answer













                          One way could be using reduce() over the new keys argument, like this:



                          sortBy = (keys, isReverse=false) => {
                          this.setState(prevState => ({
                          files: prevState.files.sort((a, b) => {
                          const valueA = keys.reduce((acc, key) => (acc || {})[key], a) || '';
                          const valueA = keys.reduce((acc, key) => (acc || {})[key], b) || '';

                          if (isReverse) return valueB.localeCompare(valueA);

                          return valueA.localeCompare(valueB);
                          })
                          }));
                          }






                          share|improve this answer












                          share|improve this answer



                          share|improve this answer










                          answered 4 hours ago









                          ShiderszShidersz

                          7,2972831




                          7,2972831























                              1














                              To work with an arbitrary number of keys, you could create a function that could be reused with .reduce() to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.






                              const getKey = (o, k) => (o || {})[k];

                              const sorter = (isReverse, ...keys) => (a, b) => {
                              const valueA = keys.reduce(getKey, a) || '';
                              const valueB = keys.reduce(getKey, b) || '';

                              if (isReverse) return valueB.localeCompare(valueA);

                              return valueA.localeCompare(valueB);
                              };

                              const sortBy = (isReverse = false, ...keys) => {
                              this.setState(prevState => ({
                              files: prevState.files.sort(sorter(isReverse, ...keys))
                              }));
                              }





                              I also moved the sort function out to its own const variable, and made it return a new function that uses the isReverse value.






                              share|improve this answer






























                                1














                                To work with an arbitrary number of keys, you could create a function that could be reused with .reduce() to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.






                                const getKey = (o, k) => (o || {})[k];

                                const sorter = (isReverse, ...keys) => (a, b) => {
                                const valueA = keys.reduce(getKey, a) || '';
                                const valueB = keys.reduce(getKey, b) || '';

                                if (isReverse) return valueB.localeCompare(valueA);

                                return valueA.localeCompare(valueB);
                                };

                                const sortBy = (isReverse = false, ...keys) => {
                                this.setState(prevState => ({
                                files: prevState.files.sort(sorter(isReverse, ...keys))
                                }));
                                }





                                I also moved the sort function out to its own const variable, and made it return a new function that uses the isReverse value.






                                share|improve this answer




























                                  1












                                  1








                                  1







                                  To work with an arbitrary number of keys, you could create a function that could be reused with .reduce() to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.






                                  const getKey = (o, k) => (o || {})[k];

                                  const sorter = (isReverse, ...keys) => (a, b) => {
                                  const valueA = keys.reduce(getKey, a) || '';
                                  const valueB = keys.reduce(getKey, b) || '';

                                  if (isReverse) return valueB.localeCompare(valueA);

                                  return valueA.localeCompare(valueB);
                                  };

                                  const sortBy = (isReverse = false, ...keys) => {
                                  this.setState(prevState => ({
                                  files: prevState.files.sort(sorter(isReverse, ...keys))
                                  }));
                                  }





                                  I also moved the sort function out to its own const variable, and made it return a new function that uses the isReverse value.






                                  share|improve this answer















                                  To work with an arbitrary number of keys, you could create a function that could be reused with .reduce() to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.






                                  const getKey = (o, k) => (o || {})[k];

                                  const sorter = (isReverse, ...keys) => (a, b) => {
                                  const valueA = keys.reduce(getKey, a) || '';
                                  const valueB = keys.reduce(getKey, b) || '';

                                  if (isReverse) return valueB.localeCompare(valueA);

                                  return valueA.localeCompare(valueB);
                                  };

                                  const sortBy = (isReverse = false, ...keys) => {
                                  this.setState(prevState => ({
                                  files: prevState.files.sort(sorter(isReverse, ...keys))
                                  }));
                                  }





                                  I also moved the sort function out to its own const variable, and made it return a new function that uses the isReverse value.






                                  const getKey = (o, k) => (o || {})[k];

                                  const sorter = (isReverse, ...keys) => (a, b) => {
                                  const valueA = keys.reduce(getKey, a) || '';
                                  const valueB = keys.reduce(getKey, b) || '';

                                  if (isReverse) return valueB.localeCompare(valueA);

                                  return valueA.localeCompare(valueB);
                                  };

                                  const sortBy = (isReverse = false, ...keys) => {
                                  this.setState(prevState => ({
                                  files: prevState.files.sort(sorter(isReverse, ...keys))
                                  }));
                                  }





                                  const getKey = (o, k) => (o || {})[k];

                                  const sorter = (isReverse, ...keys) => (a, b) => {
                                  const valueA = keys.reduce(getKey, a) || '';
                                  const valueB = keys.reduce(getKey, b) || '';

                                  if (isReverse) return valueB.localeCompare(valueA);

                                  return valueA.localeCompare(valueB);
                                  };

                                  const sortBy = (isReverse = false, ...keys) => {
                                  this.setState(prevState => ({
                                  files: prevState.files.sort(sorter(isReverse, ...keys))
                                  }));
                                  }






                                  share|improve this answer














                                  share|improve this answer



                                  share|improve this answer








                                  edited 3 hours ago

























                                  answered 4 hours ago









                                  ziggy wiggyziggy wiggy

                                  762




                                  762























                                      0














                                      Try






                                      let files = [
                                      { general: { fileID: "3"}},
                                      { general: { fileID: "1"}},
                                      { general: { fileID: "2"}},
                                      { general: { }}
                                      ];

                                      function nestedVal(keys,o) {
                                      let obj = o;
                                      keys.forEach(k=> obj = obj[k]||{} );
                                      return obj.localeCompare ? obj : '';
                                      }

                                      function sortBy(keys, arr, isReverse=false) {
                                      arr.sort((a,b) => {
                                      let av = nestedVal(keys,a);
                                      let bv = nestedVal(keys,b);
                                      let v = av.localeCompare(bv);
                                      return isReverse ? -v : v;
                                      })
                                      }

                                      sortBy(['general', 'fileID'], files, true)

                                      console.log(files);








                                      share|improve this answer






























                                        0














                                        Try






                                        let files = [
                                        { general: { fileID: "3"}},
                                        { general: { fileID: "1"}},
                                        { general: { fileID: "2"}},
                                        { general: { }}
                                        ];

                                        function nestedVal(keys,o) {
                                        let obj = o;
                                        keys.forEach(k=> obj = obj[k]||{} );
                                        return obj.localeCompare ? obj : '';
                                        }

                                        function sortBy(keys, arr, isReverse=false) {
                                        arr.sort((a,b) => {
                                        let av = nestedVal(keys,a);
                                        let bv = nestedVal(keys,b);
                                        let v = av.localeCompare(bv);
                                        return isReverse ? -v : v;
                                        })
                                        }

                                        sortBy(['general', 'fileID'], files, true)

                                        console.log(files);








                                        share|improve this answer




























                                          0












                                          0








                                          0







                                          Try






                                          let files = [
                                          { general: { fileID: "3"}},
                                          { general: { fileID: "1"}},
                                          { general: { fileID: "2"}},
                                          { general: { }}
                                          ];

                                          function nestedVal(keys,o) {
                                          let obj = o;
                                          keys.forEach(k=> obj = obj[k]||{} );
                                          return obj.localeCompare ? obj : '';
                                          }

                                          function sortBy(keys, arr, isReverse=false) {
                                          arr.sort((a,b) => {
                                          let av = nestedVal(keys,a);
                                          let bv = nestedVal(keys,b);
                                          let v = av.localeCompare(bv);
                                          return isReverse ? -v : v;
                                          })
                                          }

                                          sortBy(['general', 'fileID'], files, true)

                                          console.log(files);








                                          share|improve this answer















                                          Try






                                          let files = [
                                          { general: { fileID: "3"}},
                                          { general: { fileID: "1"}},
                                          { general: { fileID: "2"}},
                                          { general: { }}
                                          ];

                                          function nestedVal(keys,o) {
                                          let obj = o;
                                          keys.forEach(k=> obj = obj[k]||{} );
                                          return obj.localeCompare ? obj : '';
                                          }

                                          function sortBy(keys, arr, isReverse=false) {
                                          arr.sort((a,b) => {
                                          let av = nestedVal(keys,a);
                                          let bv = nestedVal(keys,b);
                                          let v = av.localeCompare(bv);
                                          return isReverse ? -v : v;
                                          })
                                          }

                                          sortBy(['general', 'fileID'], files, true)

                                          console.log(files);








                                          let files = [
                                          { general: { fileID: "3"}},
                                          { general: { fileID: "1"}},
                                          { general: { fileID: "2"}},
                                          { general: { }}
                                          ];

                                          function nestedVal(keys,o) {
                                          let obj = o;
                                          keys.forEach(k=> obj = obj[k]||{} );
                                          return obj.localeCompare ? obj : '';
                                          }

                                          function sortBy(keys, arr, isReverse=false) {
                                          arr.sort((a,b) => {
                                          let av = nestedVal(keys,a);
                                          let bv = nestedVal(keys,b);
                                          let v = av.localeCompare(bv);
                                          return isReverse ? -v : v;
                                          })
                                          }

                                          sortBy(['general', 'fileID'], files, true)

                                          console.log(files);





                                          let files = [
                                          { general: { fileID: "3"}},
                                          { general: { fileID: "1"}},
                                          { general: { fileID: "2"}},
                                          { general: { }}
                                          ];

                                          function nestedVal(keys,o) {
                                          let obj = o;
                                          keys.forEach(k=> obj = obj[k]||{} );
                                          return obj.localeCompare ? obj : '';
                                          }

                                          function sortBy(keys, arr, isReverse=false) {
                                          arr.sort((a,b) => {
                                          let av = nestedVal(keys,a);
                                          let bv = nestedVal(keys,b);
                                          let v = av.localeCompare(bv);
                                          return isReverse ? -v : v;
                                          })
                                          }

                                          sortBy(['general', 'fileID'], files, true)

                                          console.log(files);






                                          share|improve this answer














                                          share|improve this answer



                                          share|improve this answer








                                          edited 3 hours ago

























                                          answered 4 hours ago









                                          Kamil KiełczewskiKamil Kiełczewski

                                          11.3k86694




                                          11.3k86694























                                              0














                                              This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare might fail.



                                              sortBy = (keys, isReverse=false) => {
                                              this.setState(prevState => ({
                                              files: prevState.files.sort((a, b) => {
                                              const valueA = getValueAtPath(a, keys);
                                              const valueB = getValueAtPath(b, keys);

                                              if(isReverse) return valueB.localeCompare(valueA);

                                              return valueA.localeCompare(valueB);
                                              })
                                              }));
                                              }

                                              function getValueAtPath(file, path) {
                                              let value = file;
                                              let keys = [...path]; // preserve the original path array

                                              while(value && keys.length) {
                                              let key = keys.shift();
                                              value = value[key];
                                              }

                                              return (value || '').toString();
                                              }





                                              share|improve this answer






























                                                0














                                                This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare might fail.



                                                sortBy = (keys, isReverse=false) => {
                                                this.setState(prevState => ({
                                                files: prevState.files.sort((a, b) => {
                                                const valueA = getValueAtPath(a, keys);
                                                const valueB = getValueAtPath(b, keys);

                                                if(isReverse) return valueB.localeCompare(valueA);

                                                return valueA.localeCompare(valueB);
                                                })
                                                }));
                                                }

                                                function getValueAtPath(file, path) {
                                                let value = file;
                                                let keys = [...path]; // preserve the original path array

                                                while(value && keys.length) {
                                                let key = keys.shift();
                                                value = value[key];
                                                }

                                                return (value || '').toString();
                                                }





                                                share|improve this answer




























                                                  0












                                                  0








                                                  0







                                                  This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare might fail.



                                                  sortBy = (keys, isReverse=false) => {
                                                  this.setState(prevState => ({
                                                  files: prevState.files.sort((a, b) => {
                                                  const valueA = getValueAtPath(a, keys);
                                                  const valueB = getValueAtPath(b, keys);

                                                  if(isReverse) return valueB.localeCompare(valueA);

                                                  return valueA.localeCompare(valueB);
                                                  })
                                                  }));
                                                  }

                                                  function getValueAtPath(file, path) {
                                                  let value = file;
                                                  let keys = [...path]; // preserve the original path array

                                                  while(value && keys.length) {
                                                  let key = keys.shift();
                                                  value = value[key];
                                                  }

                                                  return (value || '').toString();
                                                  }





                                                  share|improve this answer















                                                  This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare might fail.



                                                  sortBy = (keys, isReverse=false) => {
                                                  this.setState(prevState => ({
                                                  files: prevState.files.sort((a, b) => {
                                                  const valueA = getValueAtPath(a, keys);
                                                  const valueB = getValueAtPath(b, keys);

                                                  if(isReverse) return valueB.localeCompare(valueA);

                                                  return valueA.localeCompare(valueB);
                                                  })
                                                  }));
                                                  }

                                                  function getValueAtPath(file, path) {
                                                  let value = file;
                                                  let keys = [...path]; // preserve the original path array

                                                  while(value && keys.length) {
                                                  let key = keys.shift();
                                                  value = value[key];
                                                  }

                                                  return (value || '').toString();
                                                  }






                                                  share|improve this answer














                                                  share|improve this answer



                                                  share|improve this answer








                                                  edited 3 hours ago

























                                                  answered 3 hours ago









                                                  abadalyanabadalyan

                                                  39128




                                                  39128























                                                      0














                                                      In this post, we'll write sortBy as -



                                                      sortBy = (comparator = asc) =>
                                                      this.setState
                                                      ( { files:
                                                      isort
                                                      ( contramap
                                                      ( comparator
                                                      , generalFileId
                                                      )
                                                      , this.state.files
                                                      )
                                                      }
                                                      )


                                                      Your question introduces you to two cool functional concepts; we'll use these to answer the question -




                                                      1. Monads

                                                      2. Contravariant Functors


                                                      Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.



                                                      We'll start by writing a function safeProp that accepts an object and a property string as input. Intuitively, safeProp safely returns the property p of object o -



                                                      const { Nothing, fromNullable } =
                                                      require ('data.maybe')

                                                      const safeProp = (o = {}, p = '') =>

                                                      // if o is an object
                                                      Object (o) === o

                                                      // access property p on object o, wrapping the result in a Maybe
                                                      ? fromNullable (o[p])

                                                      // otherwise o is not an object, return Nothing
                                                      : Nothing ()


                                                      Instead of simply returning o[p] which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -



                                                      const generalFileId = (o = {}) =>

                                                      // access the general property
                                                      safeProp (o, 'general')

                                                      // if it exists, access the fileId property on the child
                                                      .chain (child => safeProp (child, 'fileId'))

                                                      // get the result if valid, otherwise return empty string
                                                      .getOrElse ('')


                                                      Now we have a function which can take objects a varying complexity, and guarantees the result we're interested in -



                                                      console .log
                                                      ( generalFileId ({ general: { fileId: 'a' } }) // 'a'
                                                      , generalFileId ({ general: { fileId: 'b' } }) // 'b'
                                                      , generalFileId ({ general: 'x' }) // ''
                                                      , generalFileId ({ a: 'x '}) // ''
                                                      , generalFileId ({ general: { err: 'x' } }) // ''
                                                      , generalFileId ({}) // ''
                                                      )


                                                      That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.



                                                      I'm intentionally avoiding showing you an implementation of Maybe here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level. This applies regardless of whether the module is provided by a third party, such as from npm, or if you wrote the module yourself.



                                                      We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...





                                                      We start with two basic comparators, asc for ascending sort, and desc for descending sort -



                                                      const asc = (a, b) =>
                                                      a .localeCompare (b)

                                                      const desc = (a, b) =>
                                                      asc (a, b) * -1


                                                      In React, we cannot mutate previous state, instead we must create new state. So to sort, we must implement isort which will not mutate the input object -



                                                      const isort = (compare = asc, xs = ) =>
                                                      xs
                                                      .slice (0) // clone
                                                      .sort (compare) // then sort


                                                      And of course a and b are sometimes complex objects, so case we can't directly call asc or desc. Below contramap will transform our data before using one function g, before passing the data to the other function, f -



                                                      const contramap = (f, g) =>
                                                      (a, b) => f (g (a), g (b))

                                                      const files =
                                                      [ { general: { fileId: 'e' } }
                                                      , { general: { fileId: 'b' } }
                                                      , { general: { fileId: 'd' } }
                                                      , { general: { fileId: 'c' } }
                                                      , { general: { fileId: 'a' } }
                                                      ]

                                                      isort
                                                      ( contramap (asc, generalFileId) // ascending comparator
                                                      , files
                                                      )

                                                      // [ { general: { fileId: 'a' } }
                                                      // , { general: { fileId: 'b' } }
                                                      // , { general: { fileId: 'c' } }
                                                      // , { general: { fileId: 'd' } }
                                                      // , { general: { fileId: 'e' } }
                                                      // ]


                                                      Using the other comparator desc, we can see sorting work in the other direction -



                                                      isort
                                                      ( contramap (desc, generalFileId) // descending comparator
                                                      , files
                                                      )

                                                      // [ { general: { fileId: 'e' } }
                                                      // , { general: { fileId: 'd' } }
                                                      // , { general: { fileId: 'c' } }
                                                      // , { general: { fileId: 'b' } }
                                                      // , { general: { fileId: 'a' } }
                                                      // ]




                                                      Now to write the method for your React component -



                                                      sortBy = (reverse = true) =>
                                                      this.setState
                                                      ( { files:
                                                      isort
                                                      ( contramap
                                                      ( reverse ? desc : asc
                                                      , generalFileId
                                                      )
                                                      , this.state.files
                                                      )
                                                      }
                                                      )


                                                      This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -



                                                      sortBy = (comparator = asc) =>
                                                      this.setState
                                                      ( { files:
                                                      isort
                                                      ( contramap
                                                      ( comparator
                                                      , generalFileId
                                                      )
                                                      , this.state.files
                                                      )
                                                      }
                                                      )




                                                      If the nested property you need to access is not guaranteed to be general and fileId, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -



                                                      const deepProp = (o = {}, props = ) =>
                                                      props .reduce
                                                      ( (acc, p) => // for each p, safely lookup p on child
                                                      acc .chain (child => safeProp (child, p))
                                                      , fromNullable (o) // init with Maybe o
                                                      )

                                                      const generalFileId = (o = {}) =>
                                                      deepProp (o, [ 'general', 'fileId' ]) // using deepProp
                                                      .getOrElse ('')

                                                      const fooBarQux = (o = {}) =>
                                                      deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
                                                      .getOrElse (0) // customizable default

                                                      console.log
                                                      ( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
                                                      , generalFileId ({}) // ''
                                                      , fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
                                                      , fooBarQux ({ foo: { bar: 2 } }) // 0
                                                      , fooBarQux ({}) // 0
                                                      )




                                                      Above, we use the data.maybe package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable, Just and Nothing in a couple dozen lines, which we'll see later in this answer -



                                                      Run the complete demo below on repl.it



                                                      const { Just, Nothing, fromNullable } =
                                                      require ('data.maybe')

                                                      const safeProp = (o = {}, p = '') =>
                                                      Object (o) === o
                                                      ? fromNullable (o[p])
                                                      : Nothing ()

                                                      const generalFileId = (o = {}) =>
                                                      safeProp (o, 'general')
                                                      .chain (child => safeProp (child, 'fileId'))
                                                      .getOrElse ('')

                                                      // ----------------------------------------------
                                                      const asc = (a, b) =>
                                                      a .localeCompare (b)

                                                      const desc = (a, b) =>
                                                      asc (a, b) * -1

                                                      const contramap = (f, g) =>
                                                      (a, b) => f (g (a), g (b))

                                                      const isort = (compare = asc, xs = ) =>
                                                      xs
                                                      .slice (0)
                                                      .sort (compare)

                                                      // ----------------------------------------------
                                                      const files =
                                                      [ { general: { fileId: 'e' } }
                                                      , { general: { fileId: 'b' } }
                                                      , { general: { fileId: 'd' } }
                                                      , { general: { fileId: 'c' } }
                                                      , { general: { fileId: 'a' } }
                                                      ]

                                                      isort
                                                      ( contramap (asc, generalFileId)
                                                      , files
                                                      )

                                                      // [ { general: { fileId: 'a' } }
                                                      // , { general: { fileId: 'b' } }
                                                      // , { general: { fileId: 'c' } }
                                                      // , { general: { fileId: 'd' } }
                                                      // , { general: { fileId: 'e' } }
                                                      // ]


                                                      The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.



                                                      Lastly, sortBy is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse boolean; any valid comparator can be used. This means we could even write an even more specialized comparator that handles tie-breaks using custom logic or compares prop1 first, then prop2, etc. Higher-order functions expand your possibilities tremendously.





                                                      I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way provide access to the module's functionality; all other components of the module are private and free-to-change as the other requirements dictate -



                                                      // Maybe.js
                                                      const None =
                                                      Symbol ()

                                                      class Maybe
                                                      { constructor (v)
                                                      { this.value = v }

                                                      chain (f)
                                                      { return this.value == None ? this : f (this.value) }

                                                      getOrElse (v)
                                                      { return this.value === None ? v : this.value }
                                                      }

                                                      const Nothing = () =>
                                                      new Maybe (None)

                                                      const Just = v =>
                                                      new Maybe (v)

                                                      const fromNullable = v =>
                                                      v == null
                                                      ? Nothing ()
                                                      : Just (v)

                                                      module.exports =
                                                      { Just, Nothing, fromNullable } // note the class is hidden from the user


                                                      Then we would use it in our module. We only have to change the import (require) but everything else just works as-is because the public API of our module matches -



                                                      const { Just, Nothing, fromNullable } =
                                                      require ('./Maybe') // this time, use our own Maybe

                                                      const safeProp = (o = {}, p = '') => // nothing changes here
                                                      Object (o) === o
                                                      ? fromNullable (o[p])
                                                      : Nothing ()

                                                      const deepProp = (o, props) => // nothing changes here
                                                      props .reduce
                                                      ( (acc, p) =>
                                                      acc .chain (child => safeProp (child, p))
                                                      , fromNullable (o)
                                                      )

                                                      // ...




                                                      For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -




                                                      1. multi-sort using contramap

                                                      2. recursive search using contramap






                                                      share|improve this answer






























                                                        0














                                                        In this post, we'll write sortBy as -



                                                        sortBy = (comparator = asc) =>
                                                        this.setState
                                                        ( { files:
                                                        isort
                                                        ( contramap
                                                        ( comparator
                                                        , generalFileId
                                                        )
                                                        , this.state.files
                                                        )
                                                        }
                                                        )


                                                        Your question introduces you to two cool functional concepts; we'll use these to answer the question -




                                                        1. Monads

                                                        2. Contravariant Functors


                                                        Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.



                                                        We'll start by writing a function safeProp that accepts an object and a property string as input. Intuitively, safeProp safely returns the property p of object o -



                                                        const { Nothing, fromNullable } =
                                                        require ('data.maybe')

                                                        const safeProp = (o = {}, p = '') =>

                                                        // if o is an object
                                                        Object (o) === o

                                                        // access property p on object o, wrapping the result in a Maybe
                                                        ? fromNullable (o[p])

                                                        // otherwise o is not an object, return Nothing
                                                        : Nothing ()


                                                        Instead of simply returning o[p] which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -



                                                        const generalFileId = (o = {}) =>

                                                        // access the general property
                                                        safeProp (o, 'general')

                                                        // if it exists, access the fileId property on the child
                                                        .chain (child => safeProp (child, 'fileId'))

                                                        // get the result if valid, otherwise return empty string
                                                        .getOrElse ('')


                                                        Now we have a function which can take objects a varying complexity, and guarantees the result we're interested in -



                                                        console .log
                                                        ( generalFileId ({ general: { fileId: 'a' } }) // 'a'
                                                        , generalFileId ({ general: { fileId: 'b' } }) // 'b'
                                                        , generalFileId ({ general: 'x' }) // ''
                                                        , generalFileId ({ a: 'x '}) // ''
                                                        , generalFileId ({ general: { err: 'x' } }) // ''
                                                        , generalFileId ({}) // ''
                                                        )


                                                        That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.



                                                        I'm intentionally avoiding showing you an implementation of Maybe here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level. This applies regardless of whether the module is provided by a third party, such as from npm, or if you wrote the module yourself.



                                                        We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...





                                                        We start with two basic comparators, asc for ascending sort, and desc for descending sort -



                                                        const asc = (a, b) =>
                                                        a .localeCompare (b)

                                                        const desc = (a, b) =>
                                                        asc (a, b) * -1


                                                        In React, we cannot mutate previous state, instead we must create new state. So to sort, we must implement isort which will not mutate the input object -



                                                        const isort = (compare = asc, xs = ) =>
                                                        xs
                                                        .slice (0) // clone
                                                        .sort (compare) // then sort


                                                        And of course a and b are sometimes complex objects, so case we can't directly call asc or desc. Below contramap will transform our data before using one function g, before passing the data to the other function, f -



                                                        const contramap = (f, g) =>
                                                        (a, b) => f (g (a), g (b))

                                                        const files =
                                                        [ { general: { fileId: 'e' } }
                                                        , { general: { fileId: 'b' } }
                                                        , { general: { fileId: 'd' } }
                                                        , { general: { fileId: 'c' } }
                                                        , { general: { fileId: 'a' } }
                                                        ]

                                                        isort
                                                        ( contramap (asc, generalFileId) // ascending comparator
                                                        , files
                                                        )

                                                        // [ { general: { fileId: 'a' } }
                                                        // , { general: { fileId: 'b' } }
                                                        // , { general: { fileId: 'c' } }
                                                        // , { general: { fileId: 'd' } }
                                                        // , { general: { fileId: 'e' } }
                                                        // ]


                                                        Using the other comparator desc, we can see sorting work in the other direction -



                                                        isort
                                                        ( contramap (desc, generalFileId) // descending comparator
                                                        , files
                                                        )

                                                        // [ { general: { fileId: 'e' } }
                                                        // , { general: { fileId: 'd' } }
                                                        // , { general: { fileId: 'c' } }
                                                        // , { general: { fileId: 'b' } }
                                                        // , { general: { fileId: 'a' } }
                                                        // ]




                                                        Now to write the method for your React component -



                                                        sortBy = (reverse = true) =>
                                                        this.setState
                                                        ( { files:
                                                        isort
                                                        ( contramap
                                                        ( reverse ? desc : asc
                                                        , generalFileId
                                                        )
                                                        , this.state.files
                                                        )
                                                        }
                                                        )


                                                        This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -



                                                        sortBy = (comparator = asc) =>
                                                        this.setState
                                                        ( { files:
                                                        isort
                                                        ( contramap
                                                        ( comparator
                                                        , generalFileId
                                                        )
                                                        , this.state.files
                                                        )
                                                        }
                                                        )




                                                        If the nested property you need to access is not guaranteed to be general and fileId, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -



                                                        const deepProp = (o = {}, props = ) =>
                                                        props .reduce
                                                        ( (acc, p) => // for each p, safely lookup p on child
                                                        acc .chain (child => safeProp (child, p))
                                                        , fromNullable (o) // init with Maybe o
                                                        )

                                                        const generalFileId = (o = {}) =>
                                                        deepProp (o, [ 'general', 'fileId' ]) // using deepProp
                                                        .getOrElse ('')

                                                        const fooBarQux = (o = {}) =>
                                                        deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
                                                        .getOrElse (0) // customizable default

                                                        console.log
                                                        ( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
                                                        , generalFileId ({}) // ''
                                                        , fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
                                                        , fooBarQux ({ foo: { bar: 2 } }) // 0
                                                        , fooBarQux ({}) // 0
                                                        )




                                                        Above, we use the data.maybe package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable, Just and Nothing in a couple dozen lines, which we'll see later in this answer -



                                                        Run the complete demo below on repl.it



                                                        const { Just, Nothing, fromNullable } =
                                                        require ('data.maybe')

                                                        const safeProp = (o = {}, p = '') =>
                                                        Object (o) === o
                                                        ? fromNullable (o[p])
                                                        : Nothing ()

                                                        const generalFileId = (o = {}) =>
                                                        safeProp (o, 'general')
                                                        .chain (child => safeProp (child, 'fileId'))
                                                        .getOrElse ('')

                                                        // ----------------------------------------------
                                                        const asc = (a, b) =>
                                                        a .localeCompare (b)

                                                        const desc = (a, b) =>
                                                        asc (a, b) * -1

                                                        const contramap = (f, g) =>
                                                        (a, b) => f (g (a), g (b))

                                                        const isort = (compare = asc, xs = ) =>
                                                        xs
                                                        .slice (0)
                                                        .sort (compare)

                                                        // ----------------------------------------------
                                                        const files =
                                                        [ { general: { fileId: 'e' } }
                                                        , { general: { fileId: 'b' } }
                                                        , { general: { fileId: 'd' } }
                                                        , { general: { fileId: 'c' } }
                                                        , { general: { fileId: 'a' } }
                                                        ]

                                                        isort
                                                        ( contramap (asc, generalFileId)
                                                        , files
                                                        )

                                                        // [ { general: { fileId: 'a' } }
                                                        // , { general: { fileId: 'b' } }
                                                        // , { general: { fileId: 'c' } }
                                                        // , { general: { fileId: 'd' } }
                                                        // , { general: { fileId: 'e' } }
                                                        // ]


                                                        The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.



                                                        Lastly, sortBy is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse boolean; any valid comparator can be used. This means we could even write an even more specialized comparator that handles tie-breaks using custom logic or compares prop1 first, then prop2, etc. Higher-order functions expand your possibilities tremendously.





                                                        I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way provide access to the module's functionality; all other components of the module are private and free-to-change as the other requirements dictate -



                                                        // Maybe.js
                                                        const None =
                                                        Symbol ()

                                                        class Maybe
                                                        { constructor (v)
                                                        { this.value = v }

                                                        chain (f)
                                                        { return this.value == None ? this : f (this.value) }

                                                        getOrElse (v)
                                                        { return this.value === None ? v : this.value }
                                                        }

                                                        const Nothing = () =>
                                                        new Maybe (None)

                                                        const Just = v =>
                                                        new Maybe (v)

                                                        const fromNullable = v =>
                                                        v == null
                                                        ? Nothing ()
                                                        : Just (v)

                                                        module.exports =
                                                        { Just, Nothing, fromNullable } // note the class is hidden from the user


                                                        Then we would use it in our module. We only have to change the import (require) but everything else just works as-is because the public API of our module matches -



                                                        const { Just, Nothing, fromNullable } =
                                                        require ('./Maybe') // this time, use our own Maybe

                                                        const safeProp = (o = {}, p = '') => // nothing changes here
                                                        Object (o) === o
                                                        ? fromNullable (o[p])
                                                        : Nothing ()

                                                        const deepProp = (o, props) => // nothing changes here
                                                        props .reduce
                                                        ( (acc, p) =>
                                                        acc .chain (child => safeProp (child, p))
                                                        , fromNullable (o)
                                                        )

                                                        // ...




                                                        For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -




                                                        1. multi-sort using contramap

                                                        2. recursive search using contramap






                                                        share|improve this answer




























                                                          0












                                                          0








                                                          0







                                                          In this post, we'll write sortBy as -



                                                          sortBy = (comparator = asc) =>
                                                          this.setState
                                                          ( { files:
                                                          isort
                                                          ( contramap
                                                          ( comparator
                                                          , generalFileId
                                                          )
                                                          , this.state.files
                                                          )
                                                          }
                                                          )


                                                          Your question introduces you to two cool functional concepts; we'll use these to answer the question -




                                                          1. Monads

                                                          2. Contravariant Functors


                                                          Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.



                                                          We'll start by writing a function safeProp that accepts an object and a property string as input. Intuitively, safeProp safely returns the property p of object o -



                                                          const { Nothing, fromNullable } =
                                                          require ('data.maybe')

                                                          const safeProp = (o = {}, p = '') =>

                                                          // if o is an object
                                                          Object (o) === o

                                                          // access property p on object o, wrapping the result in a Maybe
                                                          ? fromNullable (o[p])

                                                          // otherwise o is not an object, return Nothing
                                                          : Nothing ()


                                                          Instead of simply returning o[p] which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -



                                                          const generalFileId = (o = {}) =>

                                                          // access the general property
                                                          safeProp (o, 'general')

                                                          // if it exists, access the fileId property on the child
                                                          .chain (child => safeProp (child, 'fileId'))

                                                          // get the result if valid, otherwise return empty string
                                                          .getOrElse ('')


                                                          Now we have a function which can take objects a varying complexity, and guarantees the result we're interested in -



                                                          console .log
                                                          ( generalFileId ({ general: { fileId: 'a' } }) // 'a'
                                                          , generalFileId ({ general: { fileId: 'b' } }) // 'b'
                                                          , generalFileId ({ general: 'x' }) // ''
                                                          , generalFileId ({ a: 'x '}) // ''
                                                          , generalFileId ({ general: { err: 'x' } }) // ''
                                                          , generalFileId ({}) // ''
                                                          )


                                                          That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.



                                                          I'm intentionally avoiding showing you an implementation of Maybe here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level. This applies regardless of whether the module is provided by a third party, such as from npm, or if you wrote the module yourself.



                                                          We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...





                                                          We start with two basic comparators, asc for ascending sort, and desc for descending sort -



                                                          const asc = (a, b) =>
                                                          a .localeCompare (b)

                                                          const desc = (a, b) =>
                                                          asc (a, b) * -1


                                                          In React, we cannot mutate previous state, instead we must create new state. So to sort, we must implement isort which will not mutate the input object -



                                                          const isort = (compare = asc, xs = ) =>
                                                          xs
                                                          .slice (0) // clone
                                                          .sort (compare) // then sort


                                                          And of course a and b are sometimes complex objects, so case we can't directly call asc or desc. Below contramap will transform our data before using one function g, before passing the data to the other function, f -



                                                          const contramap = (f, g) =>
                                                          (a, b) => f (g (a), g (b))

                                                          const files =
                                                          [ { general: { fileId: 'e' } }
                                                          , { general: { fileId: 'b' } }
                                                          , { general: { fileId: 'd' } }
                                                          , { general: { fileId: 'c' } }
                                                          , { general: { fileId: 'a' } }
                                                          ]

                                                          isort
                                                          ( contramap (asc, generalFileId) // ascending comparator
                                                          , files
                                                          )

                                                          // [ { general: { fileId: 'a' } }
                                                          // , { general: { fileId: 'b' } }
                                                          // , { general: { fileId: 'c' } }
                                                          // , { general: { fileId: 'd' } }
                                                          // , { general: { fileId: 'e' } }
                                                          // ]


                                                          Using the other comparator desc, we can see sorting work in the other direction -



                                                          isort
                                                          ( contramap (desc, generalFileId) // descending comparator
                                                          , files
                                                          )

                                                          // [ { general: { fileId: 'e' } }
                                                          // , { general: { fileId: 'd' } }
                                                          // , { general: { fileId: 'c' } }
                                                          // , { general: { fileId: 'b' } }
                                                          // , { general: { fileId: 'a' } }
                                                          // ]




                                                          Now to write the method for your React component -



                                                          sortBy = (reverse = true) =>
                                                          this.setState
                                                          ( { files:
                                                          isort
                                                          ( contramap
                                                          ( reverse ? desc : asc
                                                          , generalFileId
                                                          )
                                                          , this.state.files
                                                          )
                                                          }
                                                          )


                                                          This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -



                                                          sortBy = (comparator = asc) =>
                                                          this.setState
                                                          ( { files:
                                                          isort
                                                          ( contramap
                                                          ( comparator
                                                          , generalFileId
                                                          )
                                                          , this.state.files
                                                          )
                                                          }
                                                          )




                                                          If the nested property you need to access is not guaranteed to be general and fileId, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -



                                                          const deepProp = (o = {}, props = ) =>
                                                          props .reduce
                                                          ( (acc, p) => // for each p, safely lookup p on child
                                                          acc .chain (child => safeProp (child, p))
                                                          , fromNullable (o) // init with Maybe o
                                                          )

                                                          const generalFileId = (o = {}) =>
                                                          deepProp (o, [ 'general', 'fileId' ]) // using deepProp
                                                          .getOrElse ('')

                                                          const fooBarQux = (o = {}) =>
                                                          deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
                                                          .getOrElse (0) // customizable default

                                                          console.log
                                                          ( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
                                                          , generalFileId ({}) // ''
                                                          , fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
                                                          , fooBarQux ({ foo: { bar: 2 } }) // 0
                                                          , fooBarQux ({}) // 0
                                                          )




                                                          Above, we use the data.maybe package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable, Just and Nothing in a couple dozen lines, which we'll see later in this answer -



                                                          Run the complete demo below on repl.it



                                                          const { Just, Nothing, fromNullable } =
                                                          require ('data.maybe')

                                                          const safeProp = (o = {}, p = '') =>
                                                          Object (o) === o
                                                          ? fromNullable (o[p])
                                                          : Nothing ()

                                                          const generalFileId = (o = {}) =>
                                                          safeProp (o, 'general')
                                                          .chain (child => safeProp (child, 'fileId'))
                                                          .getOrElse ('')

                                                          // ----------------------------------------------
                                                          const asc = (a, b) =>
                                                          a .localeCompare (b)

                                                          const desc = (a, b) =>
                                                          asc (a, b) * -1

                                                          const contramap = (f, g) =>
                                                          (a, b) => f (g (a), g (b))

                                                          const isort = (compare = asc, xs = ) =>
                                                          xs
                                                          .slice (0)
                                                          .sort (compare)

                                                          // ----------------------------------------------
                                                          const files =
                                                          [ { general: { fileId: 'e' } }
                                                          , { general: { fileId: 'b' } }
                                                          , { general: { fileId: 'd' } }
                                                          , { general: { fileId: 'c' } }
                                                          , { general: { fileId: 'a' } }
                                                          ]

                                                          isort
                                                          ( contramap (asc, generalFileId)
                                                          , files
                                                          )

                                                          // [ { general: { fileId: 'a' } }
                                                          // , { general: { fileId: 'b' } }
                                                          // , { general: { fileId: 'c' } }
                                                          // , { general: { fileId: 'd' } }
                                                          // , { general: { fileId: 'e' } }
                                                          // ]


                                                          The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.



                                                          Lastly, sortBy is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse boolean; any valid comparator can be used. This means we could even write an even more specialized comparator that handles tie-breaks using custom logic or compares prop1 first, then prop2, etc. Higher-order functions expand your possibilities tremendously.





                                                          I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way provide access to the module's functionality; all other components of the module are private and free-to-change as the other requirements dictate -



                                                          // Maybe.js
                                                          const None =
                                                          Symbol ()

                                                          class Maybe
                                                          { constructor (v)
                                                          { this.value = v }

                                                          chain (f)
                                                          { return this.value == None ? this : f (this.value) }

                                                          getOrElse (v)
                                                          { return this.value === None ? v : this.value }
                                                          }

                                                          const Nothing = () =>
                                                          new Maybe (None)

                                                          const Just = v =>
                                                          new Maybe (v)

                                                          const fromNullable = v =>
                                                          v == null
                                                          ? Nothing ()
                                                          : Just (v)

                                                          module.exports =
                                                          { Just, Nothing, fromNullable } // note the class is hidden from the user


                                                          Then we would use it in our module. We only have to change the import (require) but everything else just works as-is because the public API of our module matches -



                                                          const { Just, Nothing, fromNullable } =
                                                          require ('./Maybe') // this time, use our own Maybe

                                                          const safeProp = (o = {}, p = '') => // nothing changes here
                                                          Object (o) === o
                                                          ? fromNullable (o[p])
                                                          : Nothing ()

                                                          const deepProp = (o, props) => // nothing changes here
                                                          props .reduce
                                                          ( (acc, p) =>
                                                          acc .chain (child => safeProp (child, p))
                                                          , fromNullable (o)
                                                          )

                                                          // ...




                                                          For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -




                                                          1. multi-sort using contramap

                                                          2. recursive search using contramap






                                                          share|improve this answer















                                                          In this post, we'll write sortBy as -



                                                          sortBy = (comparator = asc) =>
                                                          this.setState
                                                          ( { files:
                                                          isort
                                                          ( contramap
                                                          ( comparator
                                                          , generalFileId
                                                          )
                                                          , this.state.files
                                                          )
                                                          }
                                                          )


                                                          Your question introduces you to two cool functional concepts; we'll use these to answer the question -




                                                          1. Monads

                                                          2. Contravariant Functors


                                                          Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.



                                                          We'll start by writing a function safeProp that accepts an object and a property string as input. Intuitively, safeProp safely returns the property p of object o -



                                                          const { Nothing, fromNullable } =
                                                          require ('data.maybe')

                                                          const safeProp = (o = {}, p = '') =>

                                                          // if o is an object
                                                          Object (o) === o

                                                          // access property p on object o, wrapping the result in a Maybe
                                                          ? fromNullable (o[p])

                                                          // otherwise o is not an object, return Nothing
                                                          : Nothing ()


                                                          Instead of simply returning o[p] which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -



                                                          const generalFileId = (o = {}) =>

                                                          // access the general property
                                                          safeProp (o, 'general')

                                                          // if it exists, access the fileId property on the child
                                                          .chain (child => safeProp (child, 'fileId'))

                                                          // get the result if valid, otherwise return empty string
                                                          .getOrElse ('')


                                                          Now we have a function which can take objects a varying complexity, and guarantees the result we're interested in -



                                                          console .log
                                                          ( generalFileId ({ general: { fileId: 'a' } }) // 'a'
                                                          , generalFileId ({ general: { fileId: 'b' } }) // 'b'
                                                          , generalFileId ({ general: 'x' }) // ''
                                                          , generalFileId ({ a: 'x '}) // ''
                                                          , generalFileId ({ general: { err: 'x' } }) // ''
                                                          , generalFileId ({}) // ''
                                                          )


                                                          That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.



                                                          I'm intentionally avoiding showing you an implementation of Maybe here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level. This applies regardless of whether the module is provided by a third party, such as from npm, or if you wrote the module yourself.



                                                          We'll show a basic implementation of Maybe later in the answer, but for now we just have to finish the sort ...





                                                          We start with two basic comparators, asc for ascending sort, and desc for descending sort -



                                                          const asc = (a, b) =>
                                                          a .localeCompare (b)

                                                          const desc = (a, b) =>
                                                          asc (a, b) * -1


                                                          In React, we cannot mutate previous state, instead we must create new state. So to sort, we must implement isort which will not mutate the input object -



                                                          const isort = (compare = asc, xs = ) =>
                                                          xs
                                                          .slice (0) // clone
                                                          .sort (compare) // then sort


                                                          And of course a and b are sometimes complex objects, so case we can't directly call asc or desc. Below contramap will transform our data before using one function g, before passing the data to the other function, f -



                                                          const contramap = (f, g) =>
                                                          (a, b) => f (g (a), g (b))

                                                          const files =
                                                          [ { general: { fileId: 'e' } }
                                                          , { general: { fileId: 'b' } }
                                                          , { general: { fileId: 'd' } }
                                                          , { general: { fileId: 'c' } }
                                                          , { general: { fileId: 'a' } }
                                                          ]

                                                          isort
                                                          ( contramap (asc, generalFileId) // ascending comparator
                                                          , files
                                                          )

                                                          // [ { general: { fileId: 'a' } }
                                                          // , { general: { fileId: 'b' } }
                                                          // , { general: { fileId: 'c' } }
                                                          // , { general: { fileId: 'd' } }
                                                          // , { general: { fileId: 'e' } }
                                                          // ]


                                                          Using the other comparator desc, we can see sorting work in the other direction -



                                                          isort
                                                          ( contramap (desc, generalFileId) // descending comparator
                                                          , files
                                                          )

                                                          // [ { general: { fileId: 'e' } }
                                                          // , { general: { fileId: 'd' } }
                                                          // , { general: { fileId: 'c' } }
                                                          // , { general: { fileId: 'b' } }
                                                          // , { general: { fileId: 'a' } }
                                                          // ]




                                                          Now to write the method for your React component -



                                                          sortBy = (reverse = true) =>
                                                          this.setState
                                                          ( { files:
                                                          isort
                                                          ( contramap
                                                          ( reverse ? desc : asc
                                                          , generalFileId
                                                          )
                                                          , this.state.files
                                                          )
                                                          }
                                                          )


                                                          This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -



                                                          sortBy = (comparator = asc) =>
                                                          this.setState
                                                          ( { files:
                                                          isort
                                                          ( contramap
                                                          ( comparator
                                                          , generalFileId
                                                          )
                                                          , this.state.files
                                                          )
                                                          }
                                                          )




                                                          If the nested property you need to access is not guaranteed to be general and fileId, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -



                                                          const deepProp = (o = {}, props = ) =>
                                                          props .reduce
                                                          ( (acc, p) => // for each p, safely lookup p on child
                                                          acc .chain (child => safeProp (child, p))
                                                          , fromNullable (o) // init with Maybe o
                                                          )

                                                          const generalFileId = (o = {}) =>
                                                          deepProp (o, [ 'general', 'fileId' ]) // using deepProp
                                                          .getOrElse ('')

                                                          const fooBarQux = (o = {}) =>
                                                          deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
                                                          .getOrElse (0) // customizable default

                                                          console.log
                                                          ( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
                                                          , generalFileId ({}) // ''
                                                          , fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
                                                          , fooBarQux ({ foo: { bar: 2 } }) // 0
                                                          , fooBarQux ({}) // 0
                                                          )




                                                          Above, we use the data.maybe package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable, Just and Nothing in a couple dozen lines, which we'll see later in this answer -



                                                          Run the complete demo below on repl.it



                                                          const { Just, Nothing, fromNullable } =
                                                          require ('data.maybe')

                                                          const safeProp = (o = {}, p = '') =>
                                                          Object (o) === o
                                                          ? fromNullable (o[p])
                                                          : Nothing ()

                                                          const generalFileId = (o = {}) =>
                                                          safeProp (o, 'general')
                                                          .chain (child => safeProp (child, 'fileId'))
                                                          .getOrElse ('')

                                                          // ----------------------------------------------
                                                          const asc = (a, b) =>
                                                          a .localeCompare (b)

                                                          const desc = (a, b) =>
                                                          asc (a, b) * -1

                                                          const contramap = (f, g) =>
                                                          (a, b) => f (g (a), g (b))

                                                          const isort = (compare = asc, xs = ) =>
                                                          xs
                                                          .slice (0)
                                                          .sort (compare)

                                                          // ----------------------------------------------
                                                          const files =
                                                          [ { general: { fileId: 'e' } }
                                                          , { general: { fileId: 'b' } }
                                                          , { general: { fileId: 'd' } }
                                                          , { general: { fileId: 'c' } }
                                                          , { general: { fileId: 'a' } }
                                                          ]

                                                          isort
                                                          ( contramap (asc, generalFileId)
                                                          , files
                                                          )

                                                          // [ { general: { fileId: 'a' } }
                                                          // , { general: { fileId: 'b' } }
                                                          // , { general: { fileId: 'c' } }
                                                          // , { general: { fileId: 'd' } }
                                                          // , { general: { fileId: 'e' } }
                                                          // ]


                                                          The advantages to this approach should be evident. Instead of a one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.



                                                          Lastly, sortBy is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse boolean; any valid comparator can be used. This means we could even write an even more specialized comparator that handles tie-breaks using custom logic or compares prop1 first, then prop2, etc. Higher-order functions expand your possibilities tremendously.





                                                          I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way provide access to the module's functionality; all other components of the module are private and free-to-change as the other requirements dictate -



                                                          // Maybe.js
                                                          const None =
                                                          Symbol ()

                                                          class Maybe
                                                          { constructor (v)
                                                          { this.value = v }

                                                          chain (f)
                                                          { return this.value == None ? this : f (this.value) }

                                                          getOrElse (v)
                                                          { return this.value === None ? v : this.value }
                                                          }

                                                          const Nothing = () =>
                                                          new Maybe (None)

                                                          const Just = v =>
                                                          new Maybe (v)

                                                          const fromNullable = v =>
                                                          v == null
                                                          ? Nothing ()
                                                          : Just (v)

                                                          module.exports =
                                                          { Just, Nothing, fromNullable } // note the class is hidden from the user


                                                          Then we would use it in our module. We only have to change the import (require) but everything else just works as-is because the public API of our module matches -



                                                          const { Just, Nothing, fromNullable } =
                                                          require ('./Maybe') // this time, use our own Maybe

                                                          const safeProp = (o = {}, p = '') => // nothing changes here
                                                          Object (o) === o
                                                          ? fromNullable (o[p])
                                                          : Nothing ()

                                                          const deepProp = (o, props) => // nothing changes here
                                                          props .reduce
                                                          ( (acc, p) =>
                                                          acc .chain (child => safeProp (child, p))
                                                          , fromNullable (o)
                                                          )

                                                          // ...




                                                          For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -




                                                          1. multi-sort using contramap

                                                          2. recursive search using contramap







                                                          share|improve this answer














                                                          share|improve this answer



                                                          share|improve this answer








                                                          edited 3 mins ago

























                                                          answered 2 hours ago









                                                          user633183user633183

                                                          70.6k21139179




                                                          70.6k21139179






























                                                              draft saved

                                                              draft discarded




















































                                                              Thanks for contributing an answer to Stack Overflow!


                                                              • Please be sure to answer the question. Provide details and share your research!

                                                              But avoid



                                                              • Asking for help, clarification, or responding to other answers.

                                                              • Making statements based on opinion; back them up with references or personal experience.


                                                              To learn more, see our tips on writing great answers.




                                                              draft saved


                                                              draft discarded














                                                              StackExchange.ready(
                                                              function () {
                                                              StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54735985%2fsort-objects-in-array-with-dynamic-nested-property-keys%23new-answer', 'question_page');
                                                              }
                                                              );

                                                              Post as a guest















                                                              Required, but never shown





















































                                                              Required, but never shown














                                                              Required, but never shown












                                                              Required, but never shown







                                                              Required, but never shown

































                                                              Required, but never shown














                                                              Required, but never shown












                                                              Required, but never shown







                                                              Required, but never shown







                                                              Popular posts from this blog

                                                              what is the purpose of having a “thru cal” on RF PCB?

                                                              What does Gandalf whisper to the Moth on the Orthanc in Isengard?

                                                              magento2 creating a lot of catalogrule_product_temp tables